diff options
Diffstat (limited to 'lib/reline')
-rw-r--r-- | lib/reline/ansi.rb | 145 | ||||
-rw-r--r-- | lib/reline/config.rb | 108 | ||||
-rw-r--r-- | lib/reline/face.rb | 199 | ||||
-rw-r--r-- | lib/reline/general_io.rb | 32 | ||||
-rw-r--r-- | lib/reline/history.rb | 2 | ||||
-rw-r--r-- | lib/reline/key_actor/emacs.rb | 22 | ||||
-rw-r--r-- | lib/reline/key_actor/vi_command.rb | 46 | ||||
-rw-r--r-- | lib/reline/key_actor/vi_insert.rb | 4 | ||||
-rw-r--r-- | lib/reline/key_stroke.rb | 57 | ||||
-rw-r--r-- | lib/reline/kill_ring.rb | 4 | ||||
-rw-r--r-- | lib/reline/line_editor.rb | 2861 | ||||
-rw-r--r-- | lib/reline/reline.gemspec | 5 | ||||
-rw-r--r-- | lib/reline/terminfo.rb | 37 | ||||
-rw-r--r-- | lib/reline/unicode.rb | 93 | ||||
-rw-r--r-- | lib/reline/unicode/east_asian_width.rb | 150 | ||||
-rw-r--r-- | lib/reline/version.rb | 2 | ||||
-rw-r--r-- | lib/reline/windows.rb | 12 |
17 files changed, 1548 insertions, 2231 deletions
diff --git a/lib/reline/ansi.rb b/lib/reline/ansi.rb index ab147a6185..a3719f502c 100644 --- a/lib/reline/ansi.rb +++ b/lib/reline/ansi.rb @@ -1,20 +1,35 @@ require 'io/console' require 'io/wait' -require 'timeout' require_relative 'terminfo' class Reline::ANSI + RESET_COLOR = "\e[0m" + CAPNAME_KEY_BINDINGS = { 'khome' => :ed_move_to_beg, 'kend' => :ed_move_to_end, + 'kdch1' => :key_delete, + 'kpp' => :ed_search_prev_history, + 'knp' => :ed_search_next_history, 'kcuu1' => :ed_prev_history, 'kcud1' => :ed_next_history, 'kcuf1' => :ed_next_char, 'kcub1' => :ed_prev_char, - 'cuu' => :ed_prev_history, - 'cud' => :ed_next_history, - 'cuf' => :ed_next_char, - 'cub' => :ed_prev_char, + } + + ANSI_CURSOR_KEY_BINDINGS = { + # Up + 'A' => [:ed_prev_history, {}], + # Down + 'B' => [:ed_next_history, {}], + # Right + 'C' => [:ed_next_char, { ctrl: :em_next_word, meta: :em_next_word }], + # Left + 'D' => [:ed_prev_char, { ctrl: :ed_prev_word, meta: :ed_prev_word }], + # End + 'F' => [:ed_move_to_end, {}], + # Home + 'H' => [:ed_move_to_beg, {}], } if Reline::Terminfo.enabled? @@ -29,24 +44,14 @@ class Reline::ANSI false end - def self.set_default_key_bindings(config) - if Reline::Terminfo.enabled? + def self.set_default_key_bindings(config, allow_terminfo: true) + set_default_key_bindings_ansi_cursor(config) + if allow_terminfo && Reline::Terminfo.enabled? set_default_key_bindings_terminfo(config) else set_default_key_bindings_comprehensive_list(config) end { - # extended entries of terminfo - [27, 91, 49, 59, 53, 67] => :em_next_word, # Ctrl+→, extended entry - [27, 91, 49, 59, 53, 68] => :ed_prev_word, # Ctrl+←, extended entry - [27, 91, 49, 59, 51, 67] => :em_next_word, # Meta+→, extended entry - [27, 91, 49, 59, 51, 68] => :ed_prev_word, # Meta+←, extended entry - }.each_pair do |key, func| - config.add_default_key_binding_by_keymap(:emacs, key, func) - config.add_default_key_binding_by_keymap(:vi_insert, key, func) - config.add_default_key_binding_by_keymap(:vi_command, key, func) - end - { [27, 91, 90] => :completion_journey_up, # S-Tab }.each_pair do |key, func| config.add_default_key_binding_by_keymap(:emacs, key, func) @@ -61,18 +66,33 @@ class Reline::ANSI end end + def self.set_default_key_bindings_ansi_cursor(config) + ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)| + bindings = [["\e[#{char}", default_func]] # CSI + char + if modifiers[:ctrl] + # CSI + ctrl_key_modifier + char + bindings << ["\e[1;5#{char}", modifiers[:ctrl]] + end + if modifiers[:meta] + # CSI + meta_key_modifier + char + bindings << ["\e[1;3#{char}", modifiers[:meta]] + # Meta(ESC) + CSI + char + bindings << ["\e\e[#{char}", modifiers[:meta]] + end + bindings.each do |sequence, func| + key = sequence.bytes + config.add_default_key_binding_by_keymap(:emacs, key, func) + config.add_default_key_binding_by_keymap(:vi_insert, key, func) + config.add_default_key_binding_by_keymap(:vi_command, key, func) + end + end + end + def self.set_default_key_bindings_terminfo(config) key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding| begin key_code = Reline::Terminfo.tigetstr(capname) - case capname - # Escape sequences that omit the move distance and are set to defaults - # value 1 may be sometimes sent by pressing the arrow-key. - when 'cuu', 'cud', 'cuf', 'cub' - [ key_code.sub(/%p1%d/, '').bytes, key_binding ] - else - [ key_code.bytes, key_binding ] - end + [ key_code.bytes, key_binding ] rescue Reline::Terminfo::TerminfoError # capname is undefined end @@ -91,14 +111,8 @@ class Reline::ANSI [27, 91, 49, 126] => :ed_move_to_beg, # Home [27, 91, 52, 126] => :ed_move_to_end, # End [27, 91, 51, 126] => :key_delete, # Del - [27, 91, 65] => :ed_prev_history, # ↑ - [27, 91, 66] => :ed_next_history, # ↓ - [27, 91, 67] => :ed_next_char, # → - [27, 91, 68] => :ed_prev_char, # ← # KDE - [27, 91, 72] => :ed_move_to_beg, # Home - [27, 91, 70] => :ed_move_to_end, # End # Del is 0x08 [27, 71, 65] => :ed_prev_history, # ↑ [27, 71, 66] => :ed_next_history, # ↓ @@ -115,12 +129,6 @@ class Reline::ANSI # Del is 0x08 # Arrow keys are the same of KDE - # iTerm2 - [27, 27, 91, 67] => :em_next_word, # Option+→, extended entry - [27, 27, 91, 68] => :ed_prev_word, # Option+←, extended entry - [195, 166] => :em_next_word, # Option+f - [195, 162] => :ed_prev_word, # Option+b - [27, 79, 65] => :ed_prev_history, # ↑ [27, 79, 66] => :ed_next_history, # ↓ [27, 79, 67] => :ed_next_char, # → @@ -142,15 +150,27 @@ class Reline::ANSI @@output = val end + def self.with_raw_input + if @@input.tty? + @@input.raw(intr: true) { yield } + else + yield + end + end + @@buf = [] - def self.inner_getc + def self.inner_getc(timeout_second) unless @@buf.empty? return @@buf.shift end - until c = @@input.raw(intr: true) { @@input.wait_readable(0.1) && @@input.getbyte } - Reline.core.line_editor.resize + until @@input.wait_readable(0.01) + timeout_second -= 0.01 + return nil if timeout_second <= 0 + + Reline.core.line_editor.handle_signal end - (c == 0x16 && @@input.raw(min: 0, tim: 0, &:getbyte)) || c + c = @@input.getbyte + (c == 0x16 && @@input.raw(min: 0, time: 0, &:getbyte)) || c rescue Errno::EIO # Maybe the I/O has been closed. nil @@ -161,45 +181,43 @@ class Reline::ANSI @@in_bracketed_paste_mode = false START_BRACKETED_PASTE = String.new("\e[200~,", encoding: Encoding::ASCII_8BIT) END_BRACKETED_PASTE = String.new("\e[200~.", encoding: Encoding::ASCII_8BIT) - def self.getc_with_bracketed_paste + def self.getc_with_bracketed_paste(timeout_second) buffer = String.new(encoding: Encoding::ASCII_8BIT) - buffer << inner_getc + buffer << inner_getc(timeout_second) while START_BRACKETED_PASTE.start_with?(buffer) or END_BRACKETED_PASTE.start_with?(buffer) do if START_BRACKETED_PASTE == buffer @@in_bracketed_paste_mode = true - return inner_getc + return inner_getc(timeout_second) elsif END_BRACKETED_PASTE == buffer @@in_bracketed_paste_mode = false ungetc(-1) - return inner_getc + return inner_getc(timeout_second) end - begin - succ_c = nil - Timeout.timeout(Reline.core.config.keyseq_timeout * 100) { - succ_c = inner_getc - } - rescue Timeout::Error - break - else + succ_c = inner_getc(Reline.core.config.keyseq_timeout) + + if succ_c buffer << succ_c + else + break end end buffer.bytes.reverse_each do |ch| ungetc ch end - inner_getc + inner_getc(timeout_second) end - def self.getc + # if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second + def self.getc(timeout_second) if Reline.core.config.enable_bracketed_paste - getc_with_bracketed_paste + getc_with_bracketed_paste(timeout_second) else - inner_getc + inner_getc(timeout_second) end end def self.in_pasting? - @@in_bracketed_paste_mode or (not Reline::IOGate.empty_buffer?) + @@in_bracketed_paste_mode or (not empty_buffer?) end def self.empty_buffer? @@ -297,7 +315,7 @@ class Reline::ANSI end def self.hide_cursor - if Reline::Terminfo.enabled? + if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? begin @@output.write Reline::Terminfo.tigetstr('civis') rescue Reline::Terminfo::TerminfoError @@ -309,7 +327,7 @@ class Reline::ANSI end def self.show_cursor - if Reline::Terminfo.enabled? + if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? begin @@output.write Reline::Terminfo.tigetstr('cnorm') rescue Reline::Terminfo::TerminfoError @@ -324,9 +342,12 @@ class Reline::ANSI @@output.write "\e[K" end + # This only works when the cursor is at the bottom of the scroll range + # For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623 def self.scroll_down(x) return if x.zero? - @@output.write "\e[#{x}S" + # We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576 + @@output.write "\n" * x end def self.clear_screen diff --git a/lib/reline/config.rb b/lib/reline/config.rb index ffd1765e4f..d7564ba4b7 100644 --- a/lib/reline/config.rb +++ b/lib/reline/config.rb @@ -46,10 +46,6 @@ class Reline::Config end attr_accessor :autocompletion - attr_reader :dialog_default_bg_color_sequence, - :dialog_default_fg_color_sequence, - :dialog_highlight_bg_color_sequence, - :dialog_highlight_fg_color_sequence def initialize @additional_key_bindings = {} # from inputrc @@ -57,8 +53,6 @@ class Reline::Config @additional_key_bindings[:vi_insert] = {} @additional_key_bindings[:vi_command] = {} @oneshot_key_bindings = {} - @skip_section = nil - @if_stack = nil @editing_mode_label = :emacs @keymap_label = :emacs @keymap_prefix = [] @@ -75,10 +69,6 @@ class Reline::Config @test_mode = false @autocompletion = false @convert_meta = true if seven_bit_encoding?(Reline::IOGate.encoding) - @dialog_default_bg_color_sequence = nil - @dialog_highlight_bg_color_sequence = nil - @dialog_default_fg_color_sequence = nil - @dialog_highlight_fg_color_sequence = nil end def reset @@ -101,66 +91,7 @@ class Reline::Config end def editing_mode_is?(*val) - (val.respond_to?(:any?) ? val : [val]).any?(@editing_mode_label) - end - - def dialog_default_bg_color=(color) - @dialog_default_bg_color_sequence = dialog_color_to_code(:bg, color) - end - - def dialog_default_fg_color=(color) - @dialog_default_fg_color_sequence = dialog_color_to_code(:fg, color) - end - - def dialog_highlight_bg_color=(color) - @dialog_highlight_bg_color_sequence = dialog_color_to_code(:bg, color) - end - - def dialog_highlight_fg_color=(color) - @dialog_highlight_fg_color_sequence = dialog_color_to_code(:fg, color) - end - - def dialog_default_bg_color - dialog_code_to_color(:bg, @dialog_default_bg_color_sequence) - end - - def dialog_default_fg_color - dialog_code_to_color(:fg, @dialog_default_fg_color_sequence) - end - - def dialog_highlight_bg_color - dialog_code_to_color(:bg, @dialog_highlight_bg_color_sequence) - end - - def dialog_highlight_fg_color - dialog_code_to_color(:fg, @dialog_highlight_fg_color_sequence) - end - - COLORS = [ - :black, - :red, - :green, - :yellow, - :blue, - :magenta, - :cyan, - :white - ].freeze - - private def dialog_color_to_code(type, color) - base = type == :bg ? 40 : 30 - c = COLORS.index(color.to_sym) - - if c - base + c - else - raise ArgumentError.new("Unknown color: #{color}.\nAvailable colors: #{COLORS.join(", ")}") - end - end - - private def dialog_code_to_color(type, code) - base = type == :bg ? 40 : 30 - COLORS[code - base] + val.any?(@editing_mode_label) end def keymap @@ -257,9 +188,7 @@ class Reline::Config end end end - conditions = [@skip_section, @if_stack] - @skip_section = nil - @if_stack = [] + if_stack = [] lines.each_with_index do |line, no| next if line.match(/\A\s*#/) @@ -268,11 +197,11 @@ class Reline::Config line = line.chomp.lstrip if line.start_with?('$') - handle_directive(line[1..-1], file, no) + handle_directive(line[1..-1], file, no, if_stack) next end - next if @skip_section + next if if_stack.any? { |_no, skip| skip } case line when /^set +([^ ]+) +([^ ]+)/i @@ -286,14 +215,12 @@ class Reline::Config @additional_key_bindings[@keymap_label][@keymap_prefix + keystroke] = func end end - unless @if_stack.empty? - raise InvalidInputrc, "#{file}:#{@if_stack.last[1]}: unclosed if" + unless if_stack.empty? + raise InvalidInputrc, "#{file}:#{if_stack.last[0]}: unclosed if" end - ensure - @skip_section, @if_stack = conditions end - def handle_directive(directive, file, no) + def handle_directive(directive, file, no, if_stack) directive, args = directive.split(' ') case directive when 'if' @@ -306,20 +233,19 @@ class Reline::Config condition = true if args == 'Ruby' condition = true if args == 'Reline' end - @if_stack << [file, no, @skip_section] - @skip_section = !condition + if_stack << [no, !condition] when 'else' - if @if_stack.empty? + if if_stack.empty? raise InvalidInputrc, "#{file}:#{no}: unmatched else" end - @skip_section = !@skip_section + if_stack.last[1] = !if_stack.last[1] when 'endif' - if @if_stack.empty? + if if_stack.empty? raise InvalidInputrc, "#{file}:#{no}: unmatched endif" end - @skip_section = @if_stack.pop + if_stack.pop when 'include' - read(args) + read(File.expand_path(args)) end end @@ -395,14 +321,6 @@ class Reline::Config @vi_ins_mode_string = retrieve_string(value) when 'emacs-mode-string' @emacs_mode_string = retrieve_string(value) - when 'dialog-default-bg-color' - self.dialog_default_bg_color = value - when 'dialog-default-fg-color' - self.dialog_default_fg_color = value - when 'dialog-highlight-bg-color' - self.dialog_highlight_bg_color = value - when 'dialog-highlight-fg-color' - self.dialog_highlight_fg_color = value when *VARIABLE_NAMES then variable_name = :"@#{name.tr(?-, ?_)}" instance_variable_set(variable_name, value.nil? || value == '1' || value == 'on') diff --git a/lib/reline/face.rb b/lib/reline/face.rb new file mode 100644 index 0000000000..d07196e2e7 --- /dev/null +++ b/lib/reline/face.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +class Reline::Face + SGR_PARAMETERS = { + foreground: { + black: 30, + red: 31, + green: 32, + yellow: 33, + blue: 34, + magenta: 35, + cyan: 36, + white: 37, + bright_black: 90, + gray: 90, + bright_red: 91, + bright_green: 92, + bright_yellow: 93, + bright_blue: 94, + bright_magenta: 95, + bright_cyan: 96, + bright_white: 97 + }, + background: { + black: 40, + red: 41, + green: 42, + yellow: 43, + blue: 44, + magenta: 45, + cyan: 46, + white: 47, + bright_black: 100, + gray: 100, + bright_red: 101, + bright_green: 102, + bright_yellow: 103, + bright_blue: 104, + bright_magenta: 105, + bright_cyan: 106, + bright_white: 107, + }, + style: { + reset: 0, + bold: 1, + faint: 2, + italicized: 3, + underlined: 4, + slowly_blinking: 5, + blinking: 5, + rapidly_blinking: 6, + negative: 7, + concealed: 8, + crossed_out: 9 + } + }.freeze + + class Config + ESSENTIAL_DEFINE_NAMES = %i(default enhanced scrollbar).freeze + RESET_SGR = "\e[0m".freeze + + def initialize(name, &block) + @definition = {} + block.call(self) + ESSENTIAL_DEFINE_NAMES.each do |name| + @definition[name] ||= { style: :reset, escape_sequence: RESET_SGR } + end + end + + attr_reader :definition + + def define(name, **values) + values[:escape_sequence] = format_to_sgr(values.to_a).freeze + @definition[name] = values + end + + def reconfigure + @definition.each_value do |values| + values.delete(:escape_sequence) + values[:escape_sequence] = format_to_sgr(values.to_a).freeze + end + end + + def [](name) + @definition.dig(name, :escape_sequence) or raise ArgumentError, "unknown face: #{name}" + end + + private + + def sgr_rgb(key, value) + return nil unless rgb_expression?(value) + if Reline::Face.truecolor? + sgr_rgb_truecolor(key, value) + else + sgr_rgb_256color(key, value) + end + end + + def sgr_rgb_truecolor(key, value) + case key + when :foreground + "38;2;" + when :background + "48;2;" + end + value[1, 6].scan(/../).map(&:hex).join(";") + end + + def sgr_rgb_256color(key, value) + # 256 colors are + # 0..15: standard colors, hight intensity colors + # 16..232: 216 colors (R, G, B each 6 steps) + # 233..255: grayscale colors (24 steps) + # This methods converts rgb_expression to 216 colors + rgb = value[1, 6].scan(/../).map(&:hex) + # Color steps are [0, 95, 135, 175, 215, 255] + r, g, b = rgb.map { |v| v <= 95 ? v / 48 : (v - 35) / 40 } + color = (16 + 36 * r + 6 * g + b) + case key + when :foreground + "38;5;#{color}" + when :background + "48;5;#{color}" + end + end + + def format_to_sgr(ordered_values) + sgr = "\e[" + ordered_values.map do |key_value| + key, value = key_value + case key + when :foreground, :background + case value + when Symbol + SGR_PARAMETERS[key][value] + when String + sgr_rgb(key, value) + end + when :style + [ value ].flatten.map do |style_name| + SGR_PARAMETERS[:style][style_name] + end.then do |sgr_parameters| + sgr_parameters.include?(nil) ? nil : sgr_parameters + end + end.then do |rendition_expression| + unless rendition_expression + raise ArgumentError, "invalid SGR parameter: #{value.inspect}" + end + rendition_expression + end + end.join(';') + "m" + sgr == RESET_SGR ? RESET_SGR : RESET_SGR + sgr + end + + def rgb_expression?(color) + color.respond_to?(:match?) and color.match?(/\A#[0-9a-fA-F]{6}\z/) + end + end + + private_constant :SGR_PARAMETERS, :Config + + def self.truecolor? + @force_truecolor || %w[truecolor 24bit].include?(ENV['COLORTERM']) + end + + def self.force_truecolor + @force_truecolor = true + @configs&.each_value(&:reconfigure) + end + + def self.[](name) + @configs[name] + end + + def self.config(name, &block) + @configs ||= {} + @configs[name] = Config.new(name, &block) + end + + def self.configs + @configs.transform_values(&:definition) + end + + def self.load_initial_configs + config(:default) do |conf| + conf.define :default, style: :reset + conf.define :enhanced, style: :reset + conf.define :scrollbar, style: :reset + end + config(:completion_dialog) do |conf| + conf.define :default, foreground: :bright_white, background: :gray + conf.define :enhanced, foreground: :black, background: :white + conf.define :scrollbar, foreground: :white, background: :gray + end + end + + def self.reset_to_initial_configs + @configs = {} + load_initial_configs + end +end diff --git a/lib/reline/general_io.rb b/lib/reline/general_io.rb index 3fafad5c6e..d52151ad3c 100644 --- a/lib/reline/general_io.rb +++ b/lib/reline/general_io.rb @@ -1,10 +1,15 @@ -require 'timeout' require 'io/wait' class Reline::GeneralIO + RESET_COLOR = '' # Do not send color reset sequence + def self.reset(encoding: nil) @@pasting = false - @@encoding = encoding + if encoding + @@encoding = encoding + elsif defined?(@@encoding) + remove_class_variable(:@@encoding) + end end def self.encoding @@ -31,12 +36,17 @@ class Reline::GeneralIO @@input = val end - def self.getc + def self.with_raw_input + yield + end + + def self.getc(_timeout_second) unless @@buf.empty? return @@buf.shift end c = nil loop do + Reline.core.line_editor.handle_signal result = @@input.wait_readable(0.1) next if result.nil? c = @@input.read(1) @@ -50,13 +60,19 @@ class Reline::GeneralIO end def self.get_screen_size - [1, 1] + [24, 80] end def self.cursor_pos Reline::CursorPos.new(1, 1) end + def self.hide_cursor + end + + def self.show_cursor + end + def self.move_cursor_column(val) end @@ -87,14 +103,6 @@ class Reline::GeneralIO @@pasting end - def self.start_pasting - @@pasting = true - end - - def self.finish_pasting - @@pasting = false - end - def self.prep end diff --git a/lib/reline/history.rb b/lib/reline/history.rb index 7a1ed6b90b..3f3b65fea6 100644 --- a/lib/reline/history.rb +++ b/lib/reline/history.rb @@ -62,7 +62,7 @@ class Reline::History < Array private def check_index(index) index += size if index < 0 if index < -2147483648 or 2147483647 < index - raise RangeError.new("integer #{index} too big to convert to `int'") + raise RangeError.new("integer #{index} too big to convert to 'int'") end # If history_size is negative, history size is unlimited. if @config.history_size.positive? diff --git a/lib/reline/key_actor/emacs.rb b/lib/reline/key_actor/emacs.rb index a561feee57..5d0a7fb63d 100644 --- a/lib/reline/key_actor/emacs.rb +++ b/lib/reline/key_actor/emacs.rb @@ -49,13 +49,13 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 23 ^W :em_kill_region, # 24 ^X - :ed_sequence_lead_in, + :ed_unassigned, # 25 ^Y :em_yank, # 26 ^Z :ed_ignore, # 27 ^[ - :em_meta_next, + :ed_unassigned, # 28 ^\ :ed_ignore, # 29 ^] @@ -319,9 +319,9 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 158 M-^^ :ed_unassigned, # 159 M-^_ - :em_copy_prev_word, - # 160 M-SPACE :ed_unassigned, + # 160 M-SPACE + :em_set_mark, # 161 M-! :ed_unassigned, # 162 M-" @@ -415,7 +415,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 206 M-N :vi_search_next, # 207 M-O - :ed_sequence_lead_in, + :ed_unassigned, # 208 M-P :vi_search_prev, # 209 M-Q @@ -431,15 +431,15 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 214 M-V :ed_unassigned, # 215 M-W - :em_copy_region, + :ed_unassigned, # 216 M-X - :ed_command, - # 217 M-Y :ed_unassigned, + # 217 M-Y + :em_yank_pop, # 218 M-Z :ed_unassigned, # 219 M-[ - :ed_sequence_lead_in, + :ed_unassigned, # 220 M-\ :ed_unassigned, # 221 M-] @@ -495,9 +495,9 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 246 M-v :ed_unassigned, # 247 M-w - :em_copy_region, + :ed_unassigned, # 248 M-x - :ed_command, + :ed_unassigned, # 249 M-y :ed_unassigned, # 250 M-z diff --git a/lib/reline/key_actor/vi_command.rb b/lib/reline/key_actor/vi_command.rb index 98146d2f77..06bb0ba8e4 100644 --- a/lib/reline/key_actor/vi_command.rb +++ b/lib/reline/key_actor/vi_command.rb @@ -17,7 +17,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 7 ^G :ed_unassigned, # 8 ^H - :ed_unassigned, + :ed_prev_char, # 9 ^I :ed_unassigned, # 10 ^J @@ -41,7 +41,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 19 ^S :ed_ignore, # 20 ^T - :ed_unassigned, + :ed_transpose_chars, # 21 ^U :vi_kill_line_prev, # 22 ^V @@ -51,7 +51,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 24 ^X :ed_unassigned, # 25 ^Y - :ed_unassigned, + :em_yank, # 26 ^Z :ed_unassigned, # 27 ^[ @@ -75,7 +75,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 36 $ :ed_move_to_end, # 37 % - :vi_match, + :ed_unassigned, # 38 & :ed_unassigned, # 39 ' @@ -89,11 +89,11 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 43 + :ed_next_history, # 44 , - :vi_repeat_prev_char, + :ed_unassigned, # 45 - :ed_prev_history, # 46 . - :vi_redo, + :ed_unassigned, # 47 / :vi_search_prev, # 48 0 @@ -117,9 +117,9 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 57 9 :ed_argument_digit, # 58 : - :ed_command, + :ed_unassigned, # 59 ; - :vi_repeat_next_char, + :ed_unassigned, # 60 < :ed_unassigned, # 61 = @@ -157,21 +157,21 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 77 M :ed_unassigned, # 78 N - :vi_repeat_search_prev, + :ed_unassigned, # 79 O - :ed_sequence_lead_in, + :ed_unassigned, # 80 P :vi_paste_prev, # 81 Q :ed_unassigned, # 82 R - :vi_replace_mode, + :ed_unassigned, # 83 S - :vi_substitute_line, + :ed_unassigned, # 84 T :vi_to_prev_char, # 85 U - :vi_undo_line, + :ed_unassigned, # 86 V :ed_unassigned, # 87 W @@ -179,11 +179,11 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 88 X :ed_delete_prev_char, # 89 Y - :vi_yank_end, + :ed_unassigned, # 90 Z :ed_unassigned, # 91 [ - :ed_sequence_lead_in, + :ed_unassigned, # 92 \ :ed_unassigned, # 93 ] @@ -191,7 +191,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 94 ^ :vi_first_print, # 95 _ - :vi_history_word, + :ed_unassigned, # 96 ` :ed_unassigned, # 97 a @@ -221,7 +221,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 109 m :ed_unassigned, # 110 n - :vi_repeat_search_next, + :ed_unassigned, # 111 o :ed_unassigned, # 112 p @@ -231,11 +231,11 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 114 r :vi_replace_char, # 115 s - :vi_substitute_char, + :ed_unassigned, # 116 t :vi_to_next_char, # 117 u - :vi_undo, + :ed_unassigned, # 118 v :vi_histedit, # 119 w @@ -253,9 +253,9 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 125 } :ed_unassigned, # 126 ~ - :vi_change_case, - # 127 ^? :ed_unassigned, + # 127 ^? + :em_delete_prev_char, # 128 M-^@ :ed_unassigned, # 129 M-^A @@ -415,7 +415,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 206 M-N :ed_unassigned, # 207 M-O - :ed_sequence_lead_in, + :ed_unassigned, # 208 M-P :ed_unassigned, # 209 M-Q @@ -439,7 +439,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 218 M-Z :ed_unassigned, # 219 M-[ - :ed_sequence_lead_in, + :ed_unassigned, # 220 M-\ :ed_unassigned, # 221 M-] diff --git a/lib/reline/key_actor/vi_insert.rb b/lib/reline/key_actor/vi_insert.rb index b8e89f81d8..c3d7f9c12d 100644 --- a/lib/reline/key_actor/vi_insert.rb +++ b/lib/reline/key_actor/vi_insert.rb @@ -41,7 +41,7 @@ class Reline::KeyActor::ViInsert < Reline::KeyActor::Base # 19 ^S :vi_search_next, # 20 ^T - :ed_insert, + :ed_transpose_chars, # 21 ^U :vi_kill_line_prev, # 22 ^V @@ -51,7 +51,7 @@ class Reline::KeyActor::ViInsert < Reline::KeyActor::Base # 24 ^X :ed_insert, # 25 ^Y - :ed_insert, + :em_yank, # 26 ^Z :ed_insert, # 27 ^[ diff --git a/lib/reline/key_stroke.rb b/lib/reline/key_stroke.rb index c1c61513a9..bceffbb53f 100644 --- a/lib/reline/key_stroke.rb +++ b/lib/reline/key_stroke.rb @@ -1,4 +1,8 @@ class Reline::KeyStroke + ESC_BYTE = 27 + CSI_PARAMETER_BYTES_RANGE = 0x30..0x3f + CSI_INTERMEDIATE_BYTES_RANGE = (0x20..0x2f) + def initialize(config) @config = config end @@ -73,17 +77,26 @@ class Reline::KeyStroke return :matched if it.max_by(&:size)&.size&.< input.size return :matching if it.size > 1 } - key_mapping.keys.select { |lhs| - start_with?(input, lhs) - }.tap { |it| - return it.size > 0 ? :matched : :unmatched - } + if key_mapping.keys.any? { |lhs| start_with?(input, lhs) } + :matched + else + match_unknown_escape_sequence(input).first + end end def expand(input) - input = compress_meta_key(input) lhs = key_mapping.keys.select { |item| start_with?(input, item) }.sort_by(&:size).last - return input unless lhs + unless lhs + status, size = match_unknown_escape_sequence(input) + case status + when :matched + return [:ed_unassigned] + expand(input.drop(size)) + when :matching + return [:ed_unassigned] + else + return input + end + end rhs = key_mapping[lhs] case rhs @@ -99,6 +112,36 @@ class Reline::KeyStroke private + # returns match status of CSI/SS3 sequence and matched length + def match_unknown_escape_sequence(input) + idx = 0 + return [:unmatched, nil] unless input[idx] == ESC_BYTE + idx += 1 + idx += 1 if input[idx] == ESC_BYTE + + case input[idx] + when nil + return [:matching, nil] + when 91 # == '['.ord + # CSI sequence + idx += 1 + idx += 1 while idx < input.size && CSI_PARAMETER_BYTES_RANGE.cover?(input[idx]) + idx += 1 while idx < input.size && CSI_INTERMEDIATE_BYTES_RANGE.cover?(input[idx]) + input[idx] ? [:matched, idx + 1] : [:matching, nil] + when 79 # == 'O'.ord + # SS3 sequence + input[idx + 1] ? [:matched, idx + 2] : [:matching, nil] + else + if idx == 1 + # `ESC char`, make it :unmatched so that it will be handled correctly in `read_2nd_character_of_key_sequence` + [:unmatched, nil] + else + # `ESC ESC char` + [:matched, idx + 1] + end + end + end + def key_mapping @config.key_bindings end diff --git a/lib/reline/kill_ring.rb b/lib/reline/kill_ring.rb index bb3684b42b..201f6f3ca0 100644 --- a/lib/reline/kill_ring.rb +++ b/lib/reline/kill_ring.rb @@ -14,7 +14,7 @@ class Reline::KillRing end def ==(other) - object_id == other.object_id + equal?(other) end end @@ -68,7 +68,7 @@ class Reline::KillRing def append(string, before_p = false) case @state when State::FRESH, State::YANK - @ring << RingPoint.new(string) + @ring << RingPoint.new(+string) @state = State::CONTINUED when State::CONTINUED, State::PROCESSED if before_p diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 8d0719ef7c..81413505d7 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -6,7 +6,6 @@ require 'tempfile' class Reline::LineEditor # TODO: undo # TODO: Use "private alias_method" idiom after drop Ruby 2.5. - attr_reader :line attr_reader :byte_pointer attr_accessor :confirm_multiline_termination_proc attr_accessor :completion_proc @@ -14,7 +13,6 @@ class Reline::LineEditor attr_accessor :output_modifier_proc attr_accessor :prompt_proc attr_accessor :auto_indent_proc - attr_accessor :pre_input_hook attr_accessor :dig_perfect_match_proc attr_writer :output @@ -35,98 +33,93 @@ class Reline::LineEditor vi_next_big_word vi_prev_big_word vi_end_big_word - vi_repeat_next_char - vi_repeat_prev_char } module CompletionState NORMAL = :normal COMPLETION = :completion MENU = :menu - JOURNEY = :journey MENU_WITH_PERFECT_MATCH = :menu_with_perfect_match PERFECT_MATCH = :perfect_match end - CompletionJourneyData = Struct.new('CompletionJourneyData', :preposing, :postposing, :list, :pointer) - MenuInfo = Struct.new('MenuInfo', :target, :list) + RenderedScreen = Struct.new(:base_y, :lines, :cursor_y, keyword_init: true) - PROMPT_LIST_CACHE_TIMEOUT = 0.5 + CompletionJourneyState = Struct.new(:line_index, :pre, :target, :post, :list, :pointer) + + class MenuInfo + attr_reader :list + + def initialize(list) + @list = list + end + + def lines(screen_width) + return [] if @list.empty? + + list = @list.sort + sizes = list.map { |item| Reline::Unicode.calculate_width(item) } + item_width = sizes.max + 2 + num_cols = [screen_width / item_width, 1].max + num_rows = list.size.fdiv(num_cols).ceil + list_with_padding = list.zip(sizes).map { |item, size| item + ' ' * (item_width - size) } + aligned = (list_with_padding + [nil] * (num_rows * num_cols - list_with_padding.size)).each_slice(num_rows).to_a.transpose + aligned.map do |row| + row.join.rstrip + end + end + end + + MINIMUM_SCROLLBAR_HEIGHT = 1 def initialize(config, encoding) @config = config @completion_append_character = '' + @screen_size = Reline::IOGate.get_screen_size reset_variables(encoding: encoding) end - def set_pasting_state(in_pasting) - @in_pasting = in_pasting + def io_gate + Reline::IOGate end - def simplified_rendering? - if finished? - false - elsif @just_cursor_moving and not @rerender_all - true - else - not @rerender_all and not finished? and @in_pasting - end + def set_pasting_state(in_pasting) + # While pasting, text to be inserted is stored to @continuous_insertion_buffer. + # After pasting, this buffer should be force inserted. + process_insert(force: true) if @in_pasting && !in_pasting + @in_pasting = in_pasting end private def check_mode_string - mode_string = nil if @config.show_mode_in_prompt if @config.editing_mode_is?(:vi_command) - mode_string = @config.vi_cmd_mode_string + @config.vi_cmd_mode_string elsif @config.editing_mode_is?(:vi_insert) - mode_string = @config.vi_ins_mode_string + @config.vi_ins_mode_string elsif @config.editing_mode_is?(:emacs) - mode_string = @config.emacs_mode_string + @config.emacs_mode_string else - mode_string = '?' + '?' end end - if mode_string != @prev_mode_string - @rerender_all = true - end - @prev_mode_string = mode_string - mode_string end - private def check_multiline_prompt(buffer) + private def check_multiline_prompt(buffer, mode_string) if @vi_arg prompt = "(arg: #{@vi_arg}) " - @rerender_all = true elsif @searching_prompt prompt = @searching_prompt - @rerender_all = true else prompt = @prompt end - if simplified_rendering? + if !@is_multiline mode_string = check_mode_string prompt = mode_string + prompt if mode_string - return [prompt, calculate_width(prompt, true), [prompt] * buffer.size] - end - if @prompt_proc - use_cached_prompt_list = false - if @cached_prompt_list - if @just_cursor_moving - use_cached_prompt_list = true - elsif Time.now.to_f < (@prompt_cache_time + PROMPT_LIST_CACHE_TIMEOUT) and buffer.size == @cached_prompt_list.size - use_cached_prompt_list = true - end - end - use_cached_prompt_list = false if @rerender_all - if use_cached_prompt_list - prompt_list = @cached_prompt_list - else - prompt_list = @cached_prompt_list = @prompt_proc.(buffer).map { |pr| pr.gsub("\n", "\\n") } - @prompt_cache_time = Time.now.to_f - end + [prompt] + [''] * (buffer.size - 1) + elsif @prompt_proc + prompt_list = @prompt_proc.(buffer).map { |pr| pr.gsub("\n", "\\n") } prompt_list.map!{ prompt } if @vi_arg or @searching_prompt prompt_list = [prompt] if prompt_list.empty? - mode_string = check_mode_string prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string prompt = prompt_list[@line_index] prompt = prompt_list[0] if prompt.nil? @@ -136,24 +129,17 @@ class Reline::LineEditor prompt_list << prompt_list.last end end - prompt_width = calculate_width(prompt, true) - [prompt, prompt_width, prompt_list] + prompt_list else - mode_string = check_mode_string prompt = mode_string + prompt if mode_string - prompt_width = calculate_width(prompt, true) - [prompt, prompt_width, nil] + [prompt] * buffer.size end end def reset(prompt = '', encoding:) - @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y @screen_size = Reline::IOGate.get_screen_size - @screen_height = @screen_size.first reset_variables(prompt, encoding: encoding) - Reline::IOGate.set_winch_handler do - @resized = true - end + @rendered_screen.base_y = Reline::IOGate.cursor_pos.y if ENV.key?('RELINE_ALT_SCROLLBAR') @full_block = '::' @upper_half_block = "''" @@ -177,82 +163,57 @@ class Reline::LineEditor end end - def resize + def handle_signal + handle_interrupted + handle_resized + end + + private def handle_resized return unless @resized - @resized = false - @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y - old_screen_size = @screen_size + @screen_size = Reline::IOGate.get_screen_size - @screen_height = @screen_size.first - if old_screen_size.last < @screen_size.last # columns increase - @rerender_all = true - rerender + @resized = false + scroll_into_view + Reline::IOGate.move_cursor_up @rendered_screen.cursor_y + @rendered_screen.base_y = Reline::IOGate.cursor_pos.y + @rendered_screen.lines = [] + @rendered_screen.cursor_y = 0 + render_differential + end + + private def handle_interrupted + return unless @interrupted + + @interrupted = false + clear_dialogs + scrolldown = render_differential + Reline::IOGate.scroll_down scrolldown + Reline::IOGate.move_cursor_column 0 + @rendered_screen.lines = [] + @rendered_screen.cursor_y = 0 + case @old_trap + when 'DEFAULT', 'SYSTEM_DEFAULT' + raise Interrupt + when 'IGNORE' + # Do nothing + when 'EXIT' + exit else - back = 0 - new_buffer = whole_lines - prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer) - new_buffer.each_with_index do |line, index| - prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc - width = prompt_width + calculate_width(line) - height = calculate_height_by_width(width) - back += height - end - @highest_in_all = back - @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) - @first_line_started_from = - if @line_index.zero? - 0 - else - calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) - end - if @prompt_proc - prompt = prompt_list[@line_index] - prompt_width = calculate_width(prompt, true) - end - calculate_nearest_cursor - @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) - @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) - @rerender_all = true + @old_trap.call if @old_trap.respond_to?(:call) end end def set_signal_handlers - @old_trap = Signal.trap('INT') { - clear_dialog - if @scroll_partial_screen - move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1) - else - move_cursor_down(@highest_in_all - @line_index - 1) - end - Reline::IOGate.move_cursor_column(0) - scroll_down(1) - case @old_trap - when 'DEFAULT', 'SYSTEM_DEFAULT' - raise Interrupt - when 'IGNORE' - # Do nothing - when 'EXIT' - exit - else - @old_trap.call if @old_trap.respond_to?(:call) - end - } - begin - @old_tstp_trap = Signal.trap('TSTP') { - Reline::IOGate.ungetc("\C-z".ord) - @old_tstp_trap.call if @old_tstp_trap.respond_to?(:call) - } - rescue ArgumentError + Reline::IOGate.set_winch_handler do + @resized = true + end + @old_trap = Signal.trap('INT') do + @interrupted = true end end def finalize Signal.trap('INT', @old_trap) - begin - Signal.trap('TSTP', @old_tstp_trap) - rescue ArgumentError - end end def eof? @@ -265,55 +226,42 @@ class Reline::LineEditor @encoding = encoding @is_multiline = false @finished = false - @cleared = false - @rerender_all = false @history_pointer = nil @kill_ring ||= Reline::KillRing.new @vi_clipboard = '' @vi_arg = nil @waiting_proc = nil - @waiting_operator_proc = nil - @waiting_operator_vi_arg = nil - @completion_journey_data = nil + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + @completion_journey_state = nil @completion_state = CompletionState::NORMAL + @completion_occurs = false @perfect_matched = nil @menu_info = nil - @first_prompt = true @searching_prompt = nil @first_char = true - @add_newline_to_end_of_buffer = false - @just_cursor_moving = nil - @cached_prompt_list = nil - @prompt_cache_time = nil + @just_cursor_moving = false @eof = false @continuous_insertion_buffer = String.new(encoding: @encoding) - @scroll_partial_screen = nil - @prev_mode_string = nil + @scroll_partial_screen = 0 @drop_terminate_spaces = false @in_pasting = false @auto_indent_proc = nil @dialogs = [] - @last_key = nil + @interrupted = false @resized = false + @cache = {} + @rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0) reset_line end def reset_line - @cursor = 0 - @cursor_max = 0 @byte_pointer = 0 @buffer_of_lines = [String.new(encoding: @encoding)] @line_index = 0 - @previous_line_index = nil - @line = @buffer_of_lines[0] - @first_line_started_from = 0 - @move_up = 0 - @started_from = 0 - @highest_in_this = 1 - @highest_in_all = 1 + @cache.clear @line_backup_in_history = nil @multibyte_buffer = String.new(encoding: 'ASCII-8BIT') - @check_new_auto_indent = false end def multiline_on @@ -324,68 +272,44 @@ class Reline::LineEditor @is_multiline = false end - private def calculate_height_by_lines(lines, prompt) - result = 0 - prompt_list = prompt.is_a?(Array) ? prompt : nil - lines.each_with_index { |line, i| - prompt = prompt_list[i] if prompt_list and prompt_list[i] - result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line)) - } - result - end - private def insert_new_line(cursor_line, next_line) - @line = cursor_line @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding)) - @previous_line_index = @line_index + @buffer_of_lines[@line_index] = cursor_line @line_index += 1 - @just_cursor_moving = false - end - - private def calculate_height_by_width(width) - width.div(@screen_size.last) + 1 - end - - private def split_by_width(str, max_width) - Reline::Unicode.split_by_width(str, max_width, @encoding) - end - - private def scroll_down(val) - if val <= @rest_height - Reline::IOGate.move_cursor_down(val) - @rest_height -= val - else - Reline::IOGate.move_cursor_down(@rest_height) - Reline::IOGate.scroll_down(val - @rest_height) - @rest_height = 0 + @byte_pointer = 0 + if @auto_indent_proc && !@in_pasting + if next_line.empty? + ( + # For compatibility, use this calculation instead of just `process_auto_indent @line_index - 1, cursor_dependent: false` + indent1 = @auto_indent_proc.(@buffer_of_lines.take(@line_index - 1).push(''), @line_index - 1, 0, true) + indent2 = @auto_indent_proc.(@buffer_of_lines.take(@line_index), @line_index - 1, @buffer_of_lines[@line_index - 1].bytesize, false) + indent = indent2 || indent1 + @buffer_of_lines[@line_index - 1] = ' ' * indent + @buffer_of_lines[@line_index - 1].gsub(/\A */, '') + ) + process_auto_indent @line_index, add_newline: true + else + process_auto_indent @line_index - 1, cursor_dependent: false + process_auto_indent @line_index, add_newline: true # Need for compatibility + process_auto_indent @line_index, cursor_dependent: false + end end end - private def move_cursor_up(val) - if val > 0 - Reline::IOGate.move_cursor_up(val) - @rest_height += val - elsif val < 0 - move_cursor_down(-val) - end + private def split_by_width(str, max_width, offset: 0) + Reline::Unicode.split_by_width(str, max_width, @encoding, offset: offset) end - private def move_cursor_down(val) - if val > 0 - Reline::IOGate.move_cursor_down(val) - @rest_height -= val - @rest_height = 0 if @rest_height < 0 - elsif val < 0 - move_cursor_up(-val) - end + def current_byte_pointer_cursor + calculate_width(current_line.byteslice(0, @byte_pointer)) end - private def calculate_nearest_cursor(line_to_calc = @line, cursor = @cursor, started_from = @started_from, byte_pointer = @byte_pointer, update = true) + private def calculate_nearest_cursor(cursor) + line_to_calc = current_line new_cursor_max = calculate_width(line_to_calc) new_cursor = 0 new_byte_pointer = 0 height = 1 - max_width = @screen_size.last + max_width = screen_width if @config.editing_mode_is?(:vi_command) last_byte_size = Reline::Unicode.get_prev_mbchar_size(line_to_calc, line_to_calc.bytesize) if last_byte_size > 0 @@ -411,120 +335,237 @@ class Reline::LineEditor end new_byte_pointer += gc.bytesize end - new_started_from = height - 1 - if update - @cursor = new_cursor - @cursor_max = new_cursor_max - @started_from = new_started_from - @byte_pointer = new_byte_pointer - else - [new_cursor, new_cursor_max, new_started_from, new_byte_pointer] + @byte_pointer = new_byte_pointer + end + + def with_cache(key, *deps) + cached_deps, value = @cache[key] + if cached_deps != deps + @cache[key] = [deps, value = yield(*deps, cached_deps, value)] end + value end - def rerender_all - @rerender_all = true - process_insert(force: true) - rerender + def modified_lines + with_cache(__method__, whole_lines, finished?) do |whole, complete| + modify_lines(whole, complete) + end end - def rerender - return if @line.nil? - if @menu_info - scroll_down(@highest_in_all - @first_line_started_from) - @rerender_all = true + def prompt_list + with_cache(__method__, whole_lines, check_mode_string, @vi_arg, @searching_prompt) do |lines, mode_string| + check_multiline_prompt(lines, mode_string) end - if @menu_info - show_menu - @menu_info = nil + end + + def screen_height + @screen_size.first + end + + def screen_width + @screen_size.last + end + + def screen_scroll_top + @scroll_partial_screen + end + + def wrapped_prompt_and_input_lines + with_cache(__method__, @buffer_of_lines.size, modified_lines, prompt_list, screen_width) do |n, lines, prompts, width, prev_cache_key, cached_value| + prev_n, prev_lines, prev_prompts, prev_width = prev_cache_key + cached_wraps = {} + if prev_width == width + prev_n.times do |i| + cached_wraps[[prev_prompts[i], prev_lines[i]]] = cached_value[i] + end + end + + n.times.map do |i| + prompt = prompts[i] || '' + line = lines[i] || '' + if (cached = cached_wraps[[prompt, line]]) + next cached + end + *wrapped_prompts, code_line_prompt = split_by_width(prompt, width).first.compact + wrapped_lines = split_by_width(line, width, offset: calculate_width(code_line_prompt)).first.compact + wrapped_prompts.map { |p| [p, ''] } + [[code_line_prompt, wrapped_lines.first]] + wrapped_lines.drop(1).map { |c| ['', c] } + end end - prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines) - if @cleared - clear_screen_buffer(prompt, prompt_list, prompt_width) - @cleared = false - return + end + + def calculate_overlay_levels(overlay_levels) + levels = [] + overlay_levels.each do |x, w, l| + levels.fill(l, x, w) end - if @is_multiline and finished? and @scroll_partial_screen - # Re-output all code higher than the screen when finished. - Reline::IOGate.move_cursor_up(@first_line_started_from + @started_from - @scroll_partial_screen) - Reline::IOGate.move_cursor_column(0) - @scroll_partial_screen = nil - prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines) - if @previous_line_index - new_lines = whole_lines(index: @previous_line_index, line: @line) + levels + end + + def render_line_differential(old_items, new_items) + old_levels = calculate_overlay_levels(old_items.zip(new_items).each_with_index.map {|((x, w, c), (nx, _nw, nc)), i| [x, w, c == nc && x == nx ? i : -1] if x }.compact) + new_levels = calculate_overlay_levels(new_items.each_with_index.map { |(x, w), i| [x, w, i] if x }.compact).take(screen_width) + base_x = 0 + new_levels.zip(old_levels).chunk { |n, o| n == o ? :skip : n || :blank }.each do |level, chunk| + width = chunk.size + if level == :skip + # do nothing + elsif level == :blank + Reline::IOGate.move_cursor_column base_x + @output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}" else - new_lines = whole_lines + x, w, content = new_items[level] + content = Reline::Unicode.take_range(content, base_x - x, width) unless x == base_x && w == width + Reline::IOGate.move_cursor_column base_x + @output.write "#{Reline::IOGate::RESET_COLOR}#{content}#{Reline::IOGate::RESET_COLOR}" end - modify_lines(new_lines).each_with_index do |line, index| - @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\n" - Reline::IOGate.erase_after_cursor + base_x += width + end + if old_levels.size > new_levels.size + Reline::IOGate.move_cursor_column new_levels.size + Reline::IOGate.erase_after_cursor + end + end + + # Calculate cursor position in word wrapped content. + def wrapped_cursor_position + prompt_width = calculate_width(prompt_list[@line_index], true) + line_before_cursor = whole_lines[@line_index].byteslice(0, @byte_pointer) + wrapped_line_before_cursor = split_by_width(' ' * prompt_width + line_before_cursor, screen_width).first.compact + wrapped_cursor_y = wrapped_prompt_and_input_lines[0...@line_index].sum(&:size) + wrapped_line_before_cursor.size - 1 + wrapped_cursor_x = calculate_width(wrapped_line_before_cursor.last) + [wrapped_cursor_x, wrapped_cursor_y] + end + + def clear_dialogs + @dialogs.each do |dialog| + dialog.contents = nil + dialog.trap_key = nil + end + end + + def update_dialogs(key = nil) + wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position + @dialogs.each do |dialog| + dialog.trap_key = nil + update_each_dialog(dialog, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top, key) + end + end + + def render_finished + clear_rendered_lines + render_full_content + end + + def clear_rendered_lines + Reline::IOGate.move_cursor_up @rendered_screen.cursor_y + Reline::IOGate.move_cursor_column 0 + + num_lines = @rendered_screen.lines.size + return unless num_lines && num_lines >= 1 + + Reline::IOGate.move_cursor_down num_lines - 1 + (num_lines - 1).times do + Reline::IOGate.erase_after_cursor + Reline::IOGate.move_cursor_up 1 + end + Reline::IOGate.erase_after_cursor + @rendered_screen.lines = [] + @rendered_screen.cursor_y = 0 + end + + def render_full_content + lines = @buffer_of_lines.size.times.map do |i| + line = prompt_list[i] + modified_lines[i] + wrapped_lines, = split_by_width(line, screen_width) + wrapped_lines.last.empty? ? "#{line} " : line + end + @output.puts lines.map { |l| "#{l}\r\n" }.join + end + + def print_nomultiline_prompt(prompt) + return unless prompt && !@is_multiline + + # Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence. + @rendered_screen.lines = [[[0, Reline::Unicode.calculate_width(prompt, true), prompt]]] + @rendered_screen.cursor_y = 0 + @output.write prompt + end + + def render_differential + wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position + + rendered_lines = @rendered_screen.lines + new_lines = wrapped_prompt_and_input_lines.flatten(1)[screen_scroll_top, screen_height].map do |prompt, line| + prompt_width = Reline::Unicode.calculate_width(prompt, true) + [[0, prompt_width, prompt], [prompt_width, Reline::Unicode.calculate_width(line, true), line]] + end + if @menu_info + @menu_info.lines(screen_width).each do |item| + new_lines << [[0, Reline::Unicode.calculate_width(item), item]] end - @output.flush - clear_dialog - return + @menu_info = nil # TODO: do not change state here end - new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line)) - rendered = false - if @add_newline_to_end_of_buffer - clear_dialog_with_content - rerender_added_newline(prompt, prompt_width) - @add_newline_to_end_of_buffer = false - else - if @just_cursor_moving and not @rerender_all - clear_dialog_with_content - rendered = just_move_cursor - @just_cursor_moving = false - return - elsif @previous_line_index or new_highest_in_this != @highest_in_this - clear_dialog_with_content - rerender_changed_current_line - @previous_line_index = nil - rendered = true - elsif @rerender_all - rerender_all_lines - @rerender_all = false - rendered = true - else + + @dialogs.each_with_index do |dialog, index| + next unless dialog.contents + + x_range, y_range = dialog_range dialog, wrapped_cursor_y - screen_scroll_top + y_range.each do |row| + next if row < 0 || row >= screen_height + dialog_rows = new_lines[row] ||= [] + # index 0 is for prompt, index 1 is for line, index 2.. is for dialog + dialog_rows[index + 2] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]] end end - if @is_multiline - if finished? - # Always rerender on finish because output_modifier_proc may return a different output. - if @previous_line_index - new_lines = whole_lines(index: @previous_line_index, line: @line) - else - new_lines = whole_lines - end - line = modify_lines(new_lines)[@line_index] - clear_dialog - prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines) - render_partial(prompt, prompt_width, line, @first_line_started_from) - move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1) - scroll_down(1) - Reline::IOGate.move_cursor_column(0) - Reline::IOGate.erase_after_cursor - else - if not rendered and not @in_pasting - line = modify_lines(whole_lines)[@line_index] - prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines) - render_partial(prompt, prompt_width, line, @first_line_started_from) - end - render_dialog((prompt_width + @cursor) % @screen_size.last) + + cursor_y = @rendered_screen.cursor_y + if new_lines != rendered_lines + # Hide cursor while rendering to avoid cursor flickering. + Reline::IOGate.hide_cursor + num_lines = [[new_lines.size, rendered_lines.size].max, screen_height].min + if @rendered_screen.base_y + num_lines > screen_height + Reline::IOGate.scroll_down(num_lines - cursor_y - 1) + @rendered_screen.base_y = screen_height - num_lines + cursor_y = num_lines - 1 end - @buffer_of_lines[@line_index] = @line - @rest_height = 0 if @scroll_partial_screen - else - line = modify_lines(whole_lines)[@line_index] - render_partial(prompt, prompt_width, line, 0) - if finished? - scroll_down(1) - Reline::IOGate.move_cursor_column(0) - Reline::IOGate.erase_after_cursor + num_lines.times do |i| + rendered_line = rendered_lines[i] || [] + line_to_render = new_lines[i] || [] + next if rendered_line == line_to_render + + Reline::IOGate.move_cursor_down i - cursor_y + cursor_y = i + unless rendered_lines[i] + Reline::IOGate.move_cursor_column 0 + Reline::IOGate.erase_after_cursor + end + render_line_differential(rendered_line, line_to_render) end + @rendered_screen.lines = new_lines + Reline::IOGate.show_cursor end + y = wrapped_cursor_y - screen_scroll_top + Reline::IOGate.move_cursor_column wrapped_cursor_x + Reline::IOGate.move_cursor_down y - cursor_y + @rendered_screen.cursor_y = y + new_lines.size - y + end + + def upper_space_height(wrapped_cursor_y) + wrapped_cursor_y - screen_scroll_top + end + + def rest_height(wrapped_cursor_y) + screen_height - wrapped_cursor_y + screen_scroll_top - @rendered_screen.base_y - 1 + end + + def rerender + render_differential unless @in_pasting end class DialogProcScope + CompletionJourneyData = Struct.new(:preposing, :postposing, :list, :pointer) + def initialize(line_editor, config, proc_to_exec, context) @line_editor = line_editor @config = config @@ -575,11 +616,20 @@ class Reline::LineEditor end def screen_width - @line_editor.instance_variable_get(:@screen_size).last + @line_editor.screen_width + end + + def screen_height + @line_editor.screen_height + end + + def preferred_dialog_height + _wrapped_cursor_x, wrapped_cursor_y = @line_editor.wrapped_cursor_position + [@line_editor.upper_space_height(wrapped_cursor_y), @line_editor.rest_height(wrapped_cursor_y), (screen_height + 6) / 5].max end def completion_journey_data - @line_editor.instance_variable_get(:@completion_journey_data) + @line_editor.dialog_proc_scope_completion_journey_data end def config @@ -593,7 +643,7 @@ class Reline::LineEditor class Dialog attr_reader :name, :contents, :width - attr_accessor :scroll_top, :scrollbar_pos, :pointer, :column, :vertical_offset, :lines_backup, :trap_key + attr_accessor :scroll_top, :pointer, :column, :vertical_offset, :trap_key def initialize(name, config, proc_scope) @name = name @@ -648,49 +698,38 @@ class Reline::LineEditor end DIALOG_DEFAULT_HEIGHT = 20 - private def render_dialog(cursor_column) - @dialogs.each do |dialog| - render_each_dialog(dialog, cursor_column) - end - end private def padding_space_with_escape_sequences(str, width) - str + (' ' * (width - calculate_width(str, true))) + padding_width = width - calculate_width(str, true) + # padding_width should be only positive value. But macOS and Alacritty returns negative value. + padding_width = 0 if padding_width < 0 + str + (' ' * padding_width) end - private def render_each_dialog(dialog, cursor_column) - if @in_pasting - clear_each_dialog(dialog) - dialog.contents = nil - dialog.trap_key = nil - return - end - dialog.set_cursor_pos(cursor_column, @first_line_started_from + @started_from) - dialog_render_info = dialog.call(@last_key) + private def dialog_range(dialog, dialog_y) + x_range = dialog.column...dialog.column + dialog.width + y_range = dialog_y + dialog.vertical_offset...dialog_y + dialog.vertical_offset + dialog.contents.size + [x_range, y_range] + end + + private def update_each_dialog(dialog, cursor_column, cursor_row, key = nil) + dialog.set_cursor_pos(cursor_column, cursor_row) + dialog_render_info = dialog.call(key) if dialog_render_info.nil? or dialog_render_info.contents.nil? or dialog_render_info.contents.empty? - dialog.lines_backup = { - lines: modify_lines(whole_lines), - line_index: @line_index, - first_line_started_from: @first_line_started_from, - started_from: @started_from, - byte_pointer: @byte_pointer - } - clear_each_dialog(dialog) dialog.contents = nil dialog.trap_key = nil return end - old_dialog = dialog.clone - dialog.contents = dialog_render_info.contents + contents = dialog_render_info.contents pointer = dialog.pointer if dialog_render_info.width dialog.width = dialog_render_info.width else - dialog.width = dialog.contents.map { |l| calculate_width(l, true) }.max + dialog.width = contents.map { |l| calculate_width(l, true) }.max end height = dialog_render_info.height || DIALOG_DEFAULT_HEIGHT - height = dialog.contents.size if dialog.contents.size < height - if dialog.contents.size > height + height = contents.size if contents.size < height + if contents.size > height if dialog.pointer if dialog.pointer < 0 dialog.scroll_top = 0 @@ -700,592 +739,77 @@ class Reline::LineEditor dialog.scroll_top = dialog.pointer end pointer = dialog.pointer - dialog.scroll_top + else + dialog.scroll_top = 0 end - dialog.contents = dialog.contents[dialog.scroll_top, height] - end - if dialog.contents and dialog.scroll_top >= dialog.contents.size - dialog.scroll_top = dialog.contents.size - height + contents = contents[dialog.scroll_top, height] end if dialog_render_info.scrollbar and dialog_render_info.contents.size > height bar_max_height = height * 2 moving_distance = (dialog_render_info.contents.size - height) * 2 position_ratio = dialog.scroll_top.zero? ? 0.0 : ((dialog.scroll_top * 2).to_f / moving_distance) - bar_height = (bar_max_height * ((dialog.contents.size * 2).to_f / (dialog_render_info.contents.size * 2))).floor.to_i - dialog.scrollbar_pos = ((bar_max_height - bar_height) * position_ratio).floor.to_i + bar_height = (bar_max_height * ((contents.size * 2).to_f / (dialog_render_info.contents.size * 2))).floor.to_i + bar_height = MINIMUM_SCROLLBAR_HEIGHT if bar_height < MINIMUM_SCROLLBAR_HEIGHT + scrollbar_pos = ((bar_max_height - bar_height) * position_ratio).floor.to_i else - dialog.scrollbar_pos = nil + scrollbar_pos = nil end - upper_space = @first_line_started_from - @started_from dialog.column = dialog_render_info.pos.x - dialog.width += @block_elem_width if dialog.scrollbar_pos - diff = (dialog.column + dialog.width) - (@screen_size.last) + dialog.width += @block_elem_width if scrollbar_pos + diff = (dialog.column + dialog.width) - screen_width if diff > 0 dialog.column -= diff end - if (@rest_height - dialog_render_info.pos.y) >= height + if rest_height(screen_scroll_top + cursor_row) - dialog_render_info.pos.y >= height dialog.vertical_offset = dialog_render_info.pos.y + 1 - elsif upper_space >= height + elsif cursor_row >= height dialog.vertical_offset = dialog_render_info.pos.y - height else - if (@rest_height - dialog_render_info.pos.y) < height - scroll_down(height + dialog_render_info.pos.y) - move_cursor_up(height + dialog_render_info.pos.y) - end dialog.vertical_offset = dialog_render_info.pos.y + 1 end - Reline::IOGate.hide_cursor if dialog.column < 0 dialog.column = 0 - dialog.width = @screen_size.last - end - reset_dialog(dialog, old_dialog) - move_cursor_down(dialog.vertical_offset) - Reline::IOGate.move_cursor_column(dialog.column) - dialog.contents.each_with_index do |item, i| - if i == pointer - fg_color = dialog_render_info.pointer_fg_color - bg_color = dialog_render_info.pointer_bg_color - else - fg_color = dialog_render_info.fg_color - bg_color = dialog_render_info.bg_color - end - str_width = dialog.width - (dialog.scrollbar_pos.nil? ? 0 : @block_elem_width) + dialog.width = screen_width + end + face = Reline::Face[dialog_render_info.face || :default] + scrollbar_sgr = face[:scrollbar] + default_sgr = face[:default] + enhanced_sgr = face[:enhanced] + dialog.contents = contents.map.with_index do |item, i| + line_sgr = i == pointer ? enhanced_sgr : default_sgr + str_width = dialog.width - (scrollbar_pos.nil? ? 0 : @block_elem_width) str = padding_space_with_escape_sequences(Reline::Unicode.take_range(item, 0, str_width), str_width) - @output.write "\e[#{bg_color}m\e[#{fg_color}m#{str}" - if dialog.scrollbar_pos and (dialog.scrollbar_pos != old_dialog.scrollbar_pos or dialog.column != old_dialog.column) - @output.write "\e[37m" - if dialog.scrollbar_pos <= (i * 2) and (i * 2 + 1) < (dialog.scrollbar_pos + bar_height) - @output.write @full_block - elsif dialog.scrollbar_pos <= (i * 2) and (i * 2) < (dialog.scrollbar_pos + bar_height) - @output.write @upper_half_block - str += '' - elsif dialog.scrollbar_pos <= (i * 2 + 1) and (i * 2) < (dialog.scrollbar_pos + bar_height) - @output.write @lower_half_block - else - @output.write ' ' * @block_elem_width - end - end - @output.write "\e[0m" - Reline::IOGate.move_cursor_column(dialog.column) - move_cursor_down(1) if i < (dialog.contents.size - 1) - end - Reline::IOGate.move_cursor_column(cursor_column) - move_cursor_up(dialog.vertical_offset + dialog.contents.size - 1) - Reline::IOGate.show_cursor - dialog.lines_backup = { - lines: modify_lines(whole_lines), - line_index: @line_index, - first_line_started_from: @first_line_started_from, - started_from: @started_from, - byte_pointer: @byte_pointer - } - end - - private def reset_dialog(dialog, old_dialog) - return if dialog.lines_backup.nil? or old_dialog.contents.nil? - prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:lines]) - visual_lines = [] - visual_start = nil - dialog.lines_backup[:lines].each_with_index { |l, i| - pr = prompt_list ? prompt_list[i] : prompt - vl, _ = split_by_width(pr + l, @screen_size.last) - vl.compact! - if i == dialog.lines_backup[:line_index] - visual_start = visual_lines.size + dialog.lines_backup[:started_from] - end - visual_lines.concat(vl) - } - old_y = dialog.lines_backup[:first_line_started_from] + dialog.lines_backup[:started_from] - y = @first_line_started_from + @started_from - y_diff = y - old_y - if (old_y + old_dialog.vertical_offset) < (y + dialog.vertical_offset) - # rerender top - move_cursor_down(old_dialog.vertical_offset - y_diff) - start = visual_start + old_dialog.vertical_offset - line_num = dialog.vertical_offset - old_dialog.vertical_offset - line_num.times do |i| - Reline::IOGate.move_cursor_column(old_dialog.column) - if visual_lines[start + i].nil? - s = ' ' * old_dialog.width - else - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, old_dialog.width) - s = padding_space_with_escape_sequences(s, old_dialog.width) - end - @output.write "\e[0m#{s}\e[0m" - move_cursor_down(1) if i < (line_num - 1) - end - move_cursor_up(old_dialog.vertical_offset + line_num - 1 - y_diff) - end - if (old_y + old_dialog.vertical_offset + old_dialog.contents.size) > (y + dialog.vertical_offset + dialog.contents.size) - # rerender bottom - move_cursor_down(dialog.vertical_offset + dialog.contents.size - y_diff) - start = visual_start + dialog.vertical_offset + dialog.contents.size - line_num = (old_dialog.vertical_offset + old_dialog.contents.size) - (dialog.vertical_offset + dialog.contents.size) - line_num.times do |i| - Reline::IOGate.move_cursor_column(old_dialog.column) - if visual_lines[start + i].nil? - s = ' ' * old_dialog.width - else - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, old_dialog.width) - s = padding_space_with_escape_sequences(s, old_dialog.width) - end - @output.write "\e[0m#{s}\e[0m" - move_cursor_down(1) if i < (line_num - 1) - end - move_cursor_up(dialog.vertical_offset + dialog.contents.size + line_num - 1 - y_diff) - end - if old_dialog.column < dialog.column - # rerender left - move_cursor_down(old_dialog.vertical_offset - y_diff) - width = dialog.column - old_dialog.column - start = visual_start + old_dialog.vertical_offset - line_num = old_dialog.contents.size - line_num.times do |i| - Reline::IOGate.move_cursor_column(old_dialog.column) - if visual_lines[start + i].nil? - s = ' ' * width - else - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, width) - s = padding_space_with_escape_sequences(s, dialog.width) - end - @output.write "\e[0m#{s}\e[0m" - move_cursor_down(1) if i < (line_num - 1) - end - move_cursor_up(old_dialog.vertical_offset + line_num - 1 - y_diff) - end - if (old_dialog.column + old_dialog.width) > (dialog.column + dialog.width) - # rerender right - move_cursor_down(old_dialog.vertical_offset + y_diff) - width = (old_dialog.column + old_dialog.width) - (dialog.column + dialog.width) - start = visual_start + old_dialog.vertical_offset - line_num = old_dialog.contents.size - line_num.times do |i| - Reline::IOGate.move_cursor_column(old_dialog.column + dialog.width) - if visual_lines[start + i].nil? - s = ' ' * width - else - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column + dialog.width, width) - rerender_width = old_dialog.width - dialog.width - s = padding_space_with_escape_sequences(s, rerender_width) - end - Reline::IOGate.move_cursor_column(dialog.column + dialog.width) - @output.write "\e[0m#{s}\e[0m" - move_cursor_down(1) if i < (line_num - 1) - end - move_cursor_up(old_dialog.vertical_offset + line_num - 1 + y_diff) - end - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) - end - - private def clear_dialog - @dialogs.each do |dialog| - clear_each_dialog(dialog) - end - end - - private def clear_dialog_with_content - @dialogs.each do |dialog| - clear_each_dialog(dialog) - dialog.contents = nil - dialog.trap_key = nil - end - end - - private def clear_each_dialog(dialog) - dialog.trap_key = nil - return unless dialog.contents - prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:lines]) - visual_lines = [] - visual_lines_under_dialog = [] - visual_start = nil - dialog.lines_backup[:lines].each_with_index { |l, i| - pr = prompt_list ? prompt_list[i] : prompt - vl, _ = split_by_width(pr + l, @screen_size.last) - vl.compact! - if i == dialog.lines_backup[:line_index] - visual_start = visual_lines.size + dialog.lines_backup[:started_from] + dialog.vertical_offset - end - visual_lines.concat(vl) - } - visual_lines_under_dialog = visual_lines[visual_start, dialog.contents.size] - visual_lines_under_dialog = [] if visual_lines_under_dialog.nil? - Reline::IOGate.hide_cursor - move_cursor_down(dialog.vertical_offset) - dialog_vertical_size = dialog.contents.size - dialog_vertical_size.times do |i| - if i < visual_lines_under_dialog.size - Reline::IOGate.move_cursor_column(dialog.column) - str = Reline::Unicode.take_range(visual_lines_under_dialog[i], dialog.column, dialog.width) - str = padding_space_with_escape_sequences(str, dialog.width) - @output.write "\e[0m#{str}\e[0m" - else - Reline::IOGate.move_cursor_column(dialog.column) - @output.write "\e[0m#{' ' * dialog.width}\e[0m" - end - move_cursor_down(1) if i < (dialog_vertical_size - 1) - end - move_cursor_up(dialog_vertical_size - 1 + dialog.vertical_offset) - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) - Reline::IOGate.show_cursor - end - - private def calculate_scroll_partial_screen(highest_in_all, cursor_y) - if @screen_height < highest_in_all - old_scroll_partial_screen = @scroll_partial_screen - if cursor_y == 0 - @scroll_partial_screen = 0 - elsif cursor_y == (highest_in_all - 1) - @scroll_partial_screen = highest_in_all - @screen_height - else - if @scroll_partial_screen - if cursor_y <= @scroll_partial_screen - @scroll_partial_screen = cursor_y - elsif (@scroll_partial_screen + @screen_height - 1) < cursor_y - @scroll_partial_screen = cursor_y - (@screen_height - 1) - end - else - if cursor_y > (@screen_height - 1) - @scroll_partial_screen = cursor_y - (@screen_height - 1) - else - @scroll_partial_screen = 0 - end - end - end - if @scroll_partial_screen != old_scroll_partial_screen - @rerender_all = true - end - else - if @scroll_partial_screen - @rerender_all = true - end - @scroll_partial_screen = nil - end - end - - private def rerender_added_newline(prompt, prompt_width) - scroll_down(1) - @buffer_of_lines[@previous_line_index] = @line - @line = @buffer_of_lines[@line_index] - unless @in_pasting - render_partial(prompt, prompt_width, @line, @first_line_started_from + @started_from + 1, with_control: false) - end - @cursor = @cursor_max = calculate_width(@line) - @byte_pointer = @line.bytesize - @highest_in_all += @highest_in_this - @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) - @first_line_started_from += @started_from + 1 - @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 - @previous_line_index = nil - end - - def just_move_cursor - prompt, prompt_width, prompt_list = check_multiline_prompt(@buffer_of_lines) - move_cursor_up(@started_from) - new_first_line_started_from = - if @line_index.zero? - 0 - else - calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) - end - first_line_diff = new_first_line_started_from - @first_line_started_from - new_cursor, new_cursor_max, new_started_from, new_byte_pointer = calculate_nearest_cursor(@buffer_of_lines[@line_index], @cursor, @started_from, @byte_pointer, false) - new_started_from = calculate_height_by_width(prompt_width + new_cursor) - 1 - calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from) - @previous_line_index = nil - if @rerender_all - @line = @buffer_of_lines[@line_index] - rerender_all_lines - @rerender_all = false - true - else - @line = @buffer_of_lines[@line_index] - @first_line_started_from = new_first_line_started_from - @started_from = new_started_from - @cursor = new_cursor - @cursor_max = new_cursor_max - @byte_pointer = new_byte_pointer - move_cursor_down(first_line_diff + @started_from) - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) - false - end - end - - private def rerender_changed_current_line - if @previous_line_index - new_lines = whole_lines(index: @previous_line_index, line: @line) - else - new_lines = whole_lines - end - prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines) - all_height = calculate_height_by_lines(new_lines, prompt_list || prompt) - diff = all_height - @highest_in_all - move_cursor_down(@highest_in_all - @first_line_started_from - @started_from - 1) - if diff > 0 - scroll_down(diff) - move_cursor_up(all_height - 1) - elsif diff < 0 - (-diff).times do - Reline::IOGate.move_cursor_column(0) - Reline::IOGate.erase_after_cursor - move_cursor_up(1) - end - move_cursor_up(all_height - 1) - else - move_cursor_up(all_height - 1) - end - @highest_in_all = all_height - back = render_whole_lines(new_lines, prompt_list || prompt, prompt_width) - move_cursor_up(back) - if @previous_line_index - @buffer_of_lines[@previous_line_index] = @line - @line = @buffer_of_lines[@line_index] - end - @first_line_started_from = - if @line_index.zero? - 0 - else - calculate_height_by_lines(@buffer_of_lines[0..(@line_index - 1)], prompt_list || prompt) - end - if @prompt_proc - prompt = prompt_list[@line_index] - prompt_width = calculate_width(prompt, true) - end - move_cursor_down(@first_line_started_from) - calculate_nearest_cursor - @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 - move_cursor_down(@started_from) - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) - @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) - end - - private def rerender_all_lines - move_cursor_up(@first_line_started_from + @started_from) - Reline::IOGate.move_cursor_column(0) - back = 0 - new_buffer = whole_lines - prompt, prompt_width, prompt_list = check_multiline_prompt(new_buffer) - new_buffer.each_with_index do |line, index| - prompt_width = calculate_width(prompt_list[index], true) if @prompt_proc - width = prompt_width + calculate_width(line) - height = calculate_height_by_width(width) - back += height - end - old_highest_in_all = @highest_in_all - if @line_index.zero? - new_first_line_started_from = 0 - else - new_first_line_started_from = calculate_height_by_lines(new_buffer[0..(@line_index - 1)], prompt_list || prompt) - end - new_started_from = calculate_height_by_width(prompt_width + @cursor) - 1 - calculate_scroll_partial_screen(back, new_first_line_started_from + new_started_from) - if @scroll_partial_screen - move_cursor_up(@first_line_started_from + @started_from) - scroll_down(@screen_height - 1) - move_cursor_up(@screen_height) - Reline::IOGate.move_cursor_column(0) - elsif back > old_highest_in_all - scroll_down(back - 1) - move_cursor_up(back - 1) - elsif back < old_highest_in_all - scroll_down(back) - Reline::IOGate.erase_after_cursor - (old_highest_in_all - back - 1).times do - scroll_down(1) - Reline::IOGate.erase_after_cursor - end - move_cursor_up(old_highest_in_all - 1) - end - render_whole_lines(new_buffer, prompt_list || prompt, prompt_width) - if @prompt_proc - prompt = prompt_list[@line_index] - prompt_width = calculate_width(prompt, true) - end - @highest_in_this = calculate_height_by_width(prompt_width + @cursor_max) - @highest_in_all = back - @first_line_started_from = new_first_line_started_from - @started_from = new_started_from - if @scroll_partial_screen - Reline::IOGate.move_cursor_up(@screen_height - (@first_line_started_from + @started_from - @scroll_partial_screen) - 1) - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) - else - move_cursor_down(@first_line_started_from + @started_from - back + 1) - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) - end - end - - private def render_whole_lines(lines, prompt, prompt_width) - rendered_height = 0 - modify_lines(lines).each_with_index do |line, index| - if prompt.is_a?(Array) - line_prompt = prompt[index] - prompt_width = calculate_width(line_prompt, true) - else - line_prompt = prompt - end - height = render_partial(line_prompt, prompt_width, line, rendered_height, with_control: false) - if index < (lines.size - 1) - if @scroll_partial_screen - if (@scroll_partial_screen - height) < rendered_height and (@scroll_partial_screen + @screen_height - 1) >= (rendered_height + height) - move_cursor_down(1) - end + colored_content = "#{line_sgr}#{str}" + if scrollbar_pos + if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height) + colored_content + scrollbar_sgr + @full_block + elsif scrollbar_pos <= (i * 2) and (i * 2) < (scrollbar_pos + bar_height) + colored_content + scrollbar_sgr + @upper_half_block + elsif scrollbar_pos <= (i * 2 + 1) and (i * 2) < (scrollbar_pos + bar_height) + colored_content + scrollbar_sgr + @lower_half_block else - scroll_down(1) + colored_content + scrollbar_sgr + ' ' * @block_elem_width end - rendered_height += height else - rendered_height += height - 1 + colored_content end end - rendered_height end - private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true) - visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last) - cursor_up_from_last_line = 0 - if @scroll_partial_screen - last_visual_line = this_started_from + (height - 1) - last_screen_line = @scroll_partial_screen + (@screen_height - 1) - if (@scroll_partial_screen - this_started_from) >= height - # Render nothing because this line is before the screen. - visual_lines = [] - elsif this_started_from > last_screen_line - # Render nothing because this line is after the screen. - visual_lines = [] - else - deleted_lines_before_screen = [] - if @scroll_partial_screen > this_started_from and last_visual_line >= @scroll_partial_screen - # A part of visual lines are before the screen. - deleted_lines_before_screen = visual_lines.shift((@scroll_partial_screen - this_started_from) * 2) - deleted_lines_before_screen.compact! - end - if this_started_from <= last_screen_line and last_screen_line < last_visual_line - # A part of visual lines are after the screen. - visual_lines.pop((last_visual_line - last_screen_line) * 2) - end - move_cursor_up(deleted_lines_before_screen.size - @started_from) - cursor_up_from_last_line = @started_from - deleted_lines_before_screen.size - end - end - if with_control - if height > @highest_in_this - diff = height - @highest_in_this - scroll_down(diff) - @highest_in_all += diff - @highest_in_this = height - move_cursor_up(diff) - elsif height < @highest_in_this - diff = @highest_in_this - height - @highest_in_all -= diff - @highest_in_this = height - end - move_cursor_up(@started_from) - @started_from = calculate_height_by_width(prompt_width + @cursor) - 1 - cursor_up_from_last_line = height - 1 - @started_from - end - if Reline::Unicode::CSI_REGEXP.match?(prompt + line_to_render) - @output.write "\e[0m" # clear character decorations - end - visual_lines.each_with_index do |line, index| - Reline::IOGate.move_cursor_column(0) - if line.nil? - if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last - # reaches the end of line - if Reline::IOGate.win? and Reline::IOGate.win_legacy_console? - # A newline is automatically inserted if a character is rendered at - # eol on command prompt. - else - # When the cursor is at the end of the line and erases characters - # after the cursor, some terminals delete the character at the - # cursor position. - move_cursor_down(1) - Reline::IOGate.move_cursor_column(0) - end - else - Reline::IOGate.erase_after_cursor - move_cursor_down(1) - Reline::IOGate.move_cursor_column(0) - end - next - end - @output.write line - if Reline::IOGate.win? and Reline::IOGate.win_legacy_console? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last - # A newline is automatically inserted if a character is rendered at eol on command prompt. - @rest_height -= 1 if @rest_height > 0 - end - @output.flush - if @first_prompt - @first_prompt = false - @pre_input_hook&.call - end - end - unless visual_lines.empty? - Reline::IOGate.erase_after_cursor - Reline::IOGate.move_cursor_column(0) - end - if with_control - # Just after rendring, so the cursor is on the last line. - if finished? - Reline::IOGate.move_cursor_column(0) - else - # Moves up from bottom of lines to the cursor position. - move_cursor_up(cursor_up_from_last_line) - # This logic is buggy if a fullwidth char is wrapped because there is only one halfwidth at end of a line. - Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) - end - end - height - end - - private def modify_lines(before) - return before if before.nil? || before.empty? || simplified_rendering? - - if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: finished?) + private def modify_lines(before, complete) + if after = @output_modifier_proc&.call("#{before.join("\n")}\n", complete: complete) after.lines("\n").map { |l| l.chomp('') } else - before + before.map { |l| Reline::Unicode.escape_for_print(l) } end end - private def show_menu - scroll_down(@highest_in_all - @first_line_started_from) - @rerender_all = true - @menu_info.list.sort!.each do |item| - Reline::IOGate.move_cursor_column(0) - @output.write item - @output.flush - scroll_down(1) - end - scroll_down(@highest_in_all - 1) - move_cursor_up(@highest_in_all - 1 - @first_line_started_from) - end - - private def clear_screen_buffer(prompt, prompt_list, prompt_width) - Reline::IOGate.clear_screen - back = 0 - modify_lines(whole_lines).each_with_index do |line, index| - if @prompt_proc - pr = prompt_list[index] - height = render_partial(pr, calculate_width(pr), line, back, with_control: false) - else - height = render_partial(prompt, prompt_width, line, back, with_control: false) - end - if index < (@buffer_of_lines.size - 1) - move_cursor_down(1) - back += height - end - 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 - def editing_mode @config.editing_mode end - private def menu(target, list) - @menu_info = MenuInfo.new(target, list) + private def menu(_target, list) + @menu_info = MenuInfo.new(list) end private def complete_internal_proc(list, is_menu) @@ -1313,7 +837,7 @@ class Reline::LineEditor item_mbchars = item.grapheme_clusters end size = [memo_mbchars.size, item_mbchars.size].min - result = '' + result = +'' size.times do |i| if @config.completion_ignore_case if memo_mbchars[i].casecmp?(item_mbchars[i]) @@ -1334,9 +858,9 @@ class Reline::LineEditor [target, preposing, completed, postposing] end - private def complete(list, just_show_list = false) + private def complete(list, just_show_list) case @completion_state - when CompletionState::NORMAL, CompletionState::JOURNEY + when CompletionState::NORMAL @completion_state = CompletionState::COMPLETION when CompletionState::PERFECT_MATCH @dig_perfect_match_proc&.(@perfect_matched) @@ -1363,100 +887,79 @@ class Reline::LineEditor @completion_state = CompletionState::PERFECT_MATCH else @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH + complete(list, true) if @config.show_all_if_ambiguous end @perfect_matched = completed else @completion_state = CompletionState::MENU + complete(list, true) if @config.show_all_if_ambiguous end if not just_show_list and target < completed - @line = preposing + completed + completion_append_character.to_s + postposing - line_to_pointer = preposing + completed + completion_append_character.to_s - @cursor_max = calculate_width(@line) - @cursor = calculate_width(line_to_pointer) + @buffer_of_lines[@line_index] = (preposing + completed + completion_append_character.to_s + postposing).split("\n")[@line_index] || String.new(encoding: @encoding) + line_to_pointer = (preposing + completed + completion_append_character.to_s).split("\n")[@line_index] || String.new(encoding: @encoding) @byte_pointer = line_to_pointer.bytesize end end end - private def move_completed_list(list, direction) - case @completion_state - when CompletionState::NORMAL, CompletionState::COMPLETION, - CompletionState::MENU, CompletionState::MENU_WITH_PERFECT_MATCH - @completion_state = CompletionState::JOURNEY - result = retrieve_completion_block - return if result.nil? - preposing, target, postposing = result - @completion_journey_data = CompletionJourneyData.new( - preposing, postposing, - [target] + list.select{ |item| item.start_with?(target) }, 0) - if @completion_journey_data.list.size == 1 - @completion_journey_data.pointer = 0 - else - case direction - when :up - @completion_journey_data.pointer = @completion_journey_data.list.size - 1 - when :down - @completion_journey_data.pointer = 1 - end - end - @completion_state = CompletionState::JOURNEY - else - case direction - when :up - @completion_journey_data.pointer -= 1 - if @completion_journey_data.pointer < 0 - @completion_journey_data.pointer = @completion_journey_data.list.size - 1 - end - when :down - @completion_journey_data.pointer += 1 - if @completion_journey_data.pointer >= @completion_journey_data.list.size - @completion_journey_data.pointer = 0 - end - end + def dialog_proc_scope_completion_journey_data + return nil unless @completion_journey_state + line_index = @completion_journey_state.line_index + pre_lines = @buffer_of_lines[0...line_index].map { |line| line + "\n" } + post_lines = @buffer_of_lines[(line_index + 1)..-1].map { |line| line + "\n" } + DialogProcScope::CompletionJourneyData.new( + pre_lines.join + @completion_journey_state.pre, + @completion_journey_state.post + post_lines.join, + @completion_journey_state.list, + @completion_journey_state.pointer + ) + end + + private def move_completed_list(direction) + @completion_journey_state ||= retrieve_completion_journey_state + return false unless @completion_journey_state + + if (delta = { up: -1, down: +1 }[direction]) + @completion_journey_state.pointer = (@completion_journey_state.pointer + delta) % @completion_journey_state.list.size end - completed = @completion_journey_data.list[@completion_journey_data.pointer] - new_line = (@completion_journey_data.preposing + completed + @completion_journey_data.postposing).split("\n")[@line_index] - @line = new_line.nil? ? String.new(encoding: @encoding) : new_line - line_to_pointer = (@completion_journey_data.preposing + completed).split("\n").last - line_to_pointer = String.new(encoding: @encoding) if line_to_pointer.nil? - @cursor_max = calculate_width(@line) - @cursor = calculate_width(line_to_pointer) - @byte_pointer = line_to_pointer.bytesize + completed = @completion_journey_state.list[@completion_journey_state.pointer] + set_current_line(@completion_journey_state.pre + completed + @completion_journey_state.post, @completion_journey_state.pre.bytesize + completed.bytesize) + true + end + + private def retrieve_completion_journey_state + preposing, target, postposing = retrieve_completion_block + list = call_completion_proc + return unless list.is_a?(Array) + + candidates = list.select{ |item| item.start_with?(target) } + return if candidates.empty? + + pre = preposing.split("\n", -1).last || '' + post = postposing.split("\n", -1).first || '' + CompletionJourneyState.new( + @line_index, pre, target, post, [target] + candidates, 0 + ) end private def run_for_operators(key, method_symbol, &block) - if @waiting_operator_proc + if @vi_waiting_operator if VI_MOTIONS.include?(method_symbol) - old_cursor, old_byte_pointer = @cursor, @byte_pointer - @vi_arg = @waiting_operator_vi_arg if @waiting_operator_vi_arg > 1 + old_byte_pointer = @byte_pointer + @vi_arg = (@vi_arg || 1) * @vi_waiting_operator_arg block.(true) unless @waiting_proc - cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer - @cursor, @byte_pointer = old_cursor, old_byte_pointer - @waiting_operator_proc.(cursor_diff, byte_pointer_diff) - else - old_waiting_proc = @waiting_proc - old_waiting_operator_proc = @waiting_operator_proc - current_waiting_operator_proc = @waiting_operator_proc - @waiting_proc = proc { |k| - old_cursor, old_byte_pointer = @cursor, @byte_pointer - old_waiting_proc.(k) - cursor_diff, byte_pointer_diff = @cursor - old_cursor, @byte_pointer - old_byte_pointer - @cursor, @byte_pointer = old_cursor, old_byte_pointer - current_waiting_operator_proc.(cursor_diff, byte_pointer_diff) - @waiting_operator_proc = old_waiting_operator_proc - } + byte_pointer_diff = @byte_pointer - old_byte_pointer + @byte_pointer = old_byte_pointer + send(@vi_waiting_operator, byte_pointer_diff) + cleanup_waiting end else # Ignores operator when not motion is given. block.(false) + cleanup_waiting end - @waiting_operator_proc = nil - @waiting_operator_vi_arg = nil - if @vi_arg - @rerender_all = true - @vi_arg = nil - end + @vi_arg = nil else block.(false) end @@ -1473,7 +976,7 @@ class Reline::LineEditor end def wrap_method_call(method_symbol, method_obj, key, with_operator = false) - if @config.editing_mode_is?(:emacs, :vi_insert) and @waiting_proc.nil? and @waiting_operator_proc.nil? + if @config.editing_mode_is?(:emacs, :vi_insert) and @vi_waiting_operator.nil? not_insertion = method_symbol != :ed_insert process_insert(force: not_insertion) end @@ -1492,11 +995,32 @@ class Reline::LineEditor end end + private def cleanup_waiting + @waiting_proc = nil + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + @searching_prompt = nil + @drop_terminate_spaces = false + end + private def process_key(key, method_symbol) + if key.is_a?(Symbol) + cleanup_waiting + elsif @waiting_proc + old_byte_pointer = @byte_pointer + @waiting_proc.call(key) + if @vi_waiting_operator + byte_pointer_diff = @byte_pointer - old_byte_pointer + @byte_pointer = old_byte_pointer + send(@vi_waiting_operator, byte_pointer_diff) + cleanup_waiting + end + @kill_ring.process + return + end + if method_symbol and respond_to?(method_symbol, true) method_obj = method(method_symbol) - else - method_obj = nil end if method_symbol and key.is_a?(Symbol) if @vi_arg and argumentable?(method_obj) @@ -1508,7 +1032,6 @@ class Reline::LineEditor end @kill_ring.process if @vi_arg - @rerender_al = true @vi_arg = nil end elsif @vi_arg @@ -1519,8 +1042,6 @@ class Reline::LineEditor run_for_operators(key, method_symbol) do |with_operator| wrap_method_call(method_symbol, method_obj, key, with_operator) end - elsif @waiting_proc - @waiting_proc.(key) elsif method_obj wrap_method_call(method_symbol, method_obj, key) else @@ -1528,13 +1049,9 @@ class Reline::LineEditor end @kill_ring.process if @vi_arg - @rerender_all = true @vi_arg = nil end end - elsif @waiting_proc - @waiting_proc.(key) - @kill_ring.process elsif method_obj if method_symbol == :ed_argument_digit wrap_method_call(method_symbol, method_obj, key) @@ -1550,7 +1067,6 @@ class Reline::LineEditor end private def normal_char(key) - method_symbol = method_obj = nil if key.combined_char.is_a?(Symbol) process_key(key.combined_char, key.combined_char) return @@ -1568,97 +1084,104 @@ class Reline::LineEditor return if key.char >= 128 # maybe, first byte of multi byte method_symbol = @config.editing_mode.get_method(key.combined_char) if key.with_meta and method_symbol == :ed_unassigned - # split ESC + key - method_symbol = @config.editing_mode.get_method("\e".ord) - process_key("\e".ord, method_symbol) - method_symbol = @config.editing_mode.get_method(key.char) - process_key(key.char, method_symbol) + if @config.editing_mode_is?(:vi_command, :vi_insert) + # split ESC + key in vi mode + method_symbol = @config.editing_mode.get_method("\e".ord) + process_key("\e".ord, method_symbol) + method_symbol = @config.editing_mode.get_method(key.char) + process_key(key.char, method_symbol) + end else process_key(key.combined_char, method_symbol) end @multibyte_buffer.clear end - if @config.editing_mode_is?(:vi_command) and @cursor > 0 and @cursor == @cursor_max - byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) + if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize + byte_size = Reline::Unicode.get_prev_mbchar_size(@buffer_of_lines[@line_index], @byte_pointer) @byte_pointer -= byte_size - mbchar = @line.byteslice(@byte_pointer, byte_size) - width = Reline::Unicode.get_mbchar_width(mbchar) - @cursor -= width + end + end + + def update(key) + modified = input_key(key) + unless @in_pasting + scroll_into_view + @just_cursor_moving = !modified + update_dialogs(key) + @just_cursor_moving = false end end def input_key(key) - @last_key = key @config.reset_oneshot_key_bindings @dialogs.each do |dialog| if key.char.instance_of?(Symbol) and key.char == dialog.name return end end - @just_cursor_moving = nil if key.char.nil? if @first_char - @line = nil + @eof = true end finish return end - old_line = @line.dup + old_lines = @buffer_of_lines.dup @first_char = false - completion_occurs = false + @completion_occurs = false if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord - unless @config.disable_completion - result = call_completion_proc - if result.is_a?(Array) - completion_occurs = true - process_insert - if @config.autocompletion - move_completed_list(result, :down) - else - complete(result) + if !@config.disable_completion + process_insert(force: true) + if @config.autocompletion + @completion_state = CompletionState::NORMAL + @completion_occurs = move_completed_list(:down) + else + @completion_journey_state = nil + result = call_completion_proc + if result.is_a?(Array) + @completion_occurs = true + complete(result, false) end end end - elsif @config.editing_mode_is?(:emacs, :vi_insert) and key.char == :completion_journey_up - if not @config.disable_completion and @config.autocompletion - result = call_completion_proc - if result.is_a?(Array) - completion_occurs = true - process_insert - move_completed_list(result, :up) - end - end - elsif not @config.disable_completion and @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char) - unless @config.disable_completion - result = call_completion_proc - if result.is_a?(Array) - completion_occurs = true - process_insert - move_completed_list(result, "\C-p".ord == key.char ? :up : :down) - end + elsif @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char) + # In vi mode, move completed list even if autocompletion is off + if not @config.disable_completion + process_insert(force: true) + @completion_state = CompletionState::NORMAL + @completion_occurs = move_completed_list("\C-p".ord == key.char ? :up : :down) end elsif Symbol === key.char and respond_to?(key.char, true) process_key(key.char, key.char) else normal_char(key) end - unless completion_occurs + unless @completion_occurs @completion_state = CompletionState::NORMAL - @completion_journey_data = nil + @completion_journey_state = nil end - if not @in_pasting and @just_cursor_moving.nil? - if @previous_line_index and @buffer_of_lines[@previous_line_index] == @line - @just_cursor_moving = true - elsif @previous_line_index.nil? and @buffer_of_lines[@line_index] == @line and old_line == @line - @just_cursor_moving = true - else - @just_cursor_moving = false - end - else - @just_cursor_moving = false + + if @in_pasting + clear_dialogs + return + end + + modified = old_lines != @buffer_of_lines + if !@completion_occurs && modified && !@config.disable_completion && @config.autocompletion + # Auto complete starts only when edited + process_insert(force: true) + @completion_journey_state = retrieve_completion_journey_state + end + modified + end + + def scroll_into_view + _wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position + if wrapped_cursor_y < screen_scroll_top + @scroll_partial_screen = wrapped_cursor_y end - if @is_multiline and @auto_indent_proc and not simplified_rendering? - process_auto_indent + if wrapped_cursor_y >= screen_scroll_top + screen_height + @scroll_partial_screen = wrapped_cursor_y - screen_height + 1 end end @@ -1692,46 +1215,40 @@ class Reline::LineEditor result end - private def process_auto_indent - return if not @check_new_auto_indent and @previous_line_index # move cursor up or down - if @check_new_auto_indent and @previous_line_index and @previous_line_index > 0 and @line_index > @previous_line_index - # Fix indent of a line when a newline is inserted to the next - new_lines = whole_lines(index: @previous_line_index, line: @line) - new_indent = @auto_indent_proc.(new_lines[0..-3].push(''), @line_index - 1, 0, true) - md = @line.match(/\A */) - prev_indent = md[0].count(' ') - @line = ' ' * new_indent + @line.lstrip + private def process_auto_indent(line_index = @line_index, cursor_dependent: true, add_newline: false) + return if @in_pasting + return unless @auto_indent_proc - new_indent = nil - result = @auto_indent_proc.(new_lines[0..-2], @line_index - 1, (new_lines[-2].size + 1), false) - if result - new_indent = result - end - if new_indent&.>= 0 - @line = ' ' * new_indent + @line.lstrip - end + line = @buffer_of_lines[line_index] + byte_pointer = cursor_dependent && @line_index == line_index ? @byte_pointer : line.bytesize + new_indent = @auto_indent_proc.(@buffer_of_lines.take(line_index + 1).push(''), line_index, byte_pointer, add_newline) + return unless new_indent + + new_line = ' ' * new_indent + line.lstrip + @buffer_of_lines[line_index] = new_line + if @line_index == line_index + indent_diff = new_line.bytesize - line.bytesize + @byte_pointer = [@byte_pointer + indent_diff, 0].max end - if @previous_line_index - new_lines = whole_lines(index: @previous_line_index, line: @line) + end + + def line() + @buffer_of_lines.join("\n") unless eof? + end + + def current_line + @buffer_of_lines[@line_index] + end + + def set_current_line(line, byte_pointer = nil) + cursor = current_byte_pointer_cursor + @buffer_of_lines[@line_index] = line + if byte_pointer + @byte_pointer = byte_pointer else - new_lines = whole_lines - end - new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent) - new_indent = @cursor_max if new_indent&.> @cursor_max - if new_indent&.>= 0 - md = new_lines[@line_index].match(/\A */) - prev_indent = md[0].count(' ') - if @check_new_auto_indent - @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip - @cursor = new_indent - @byte_pointer = new_indent - else - @line = ' ' * new_indent + @line.lstrip - @cursor += new_indent - prev_indent - @byte_pointer += new_indent - prev_indent - end + calculate_nearest_cursor(cursor) end - @check_new_auto_indent = false + process_auto_indent end def retrieve_completion_block(set_completion_quote_character = false) @@ -1745,7 +1262,7 @@ class Reline::LineEditor else quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ end - before = @line.byteslice(0, @byte_pointer) + before = current_line.byteslice(0, @byte_pointer) rest = nil break_pointer = nil quote = nil @@ -1753,7 +1270,7 @@ class Reline::LineEditor escaped_quote = nil i = 0 while i < @byte_pointer do - slice = @line.byteslice(i, @byte_pointer - i) + slice = current_line.byteslice(i, @byte_pointer - i) unless slice.valid_encoding? i += 1 next @@ -1775,15 +1292,15 @@ class Reline::LineEditor elsif word_break_regexp and not quote and slice =~ word_break_regexp rest = $' i += 1 - before = @line.byteslice(i, @byte_pointer - i) + before = current_line.byteslice(i, @byte_pointer - i) break_pointer = i else i += 1 end end - postposing = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer) + postposing = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer) if rest - preposing = @line.byteslice(0, break_pointer) + preposing = current_line.byteslice(0, break_pointer) target = rest if set_completion_quote_character and quote Reline.core.instance_variable_set(:@completion_quote_character, quote) @@ -1794,133 +1311,81 @@ class Reline::LineEditor else preposing = '' if break_pointer - preposing = @line.byteslice(0, break_pointer) + preposing = current_line.byteslice(0, break_pointer) else preposing = '' 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 + lines = whole_lines + 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 [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)] end def confirm_multiline_termination temp_buffer = @buffer_of_lines.dup - if @previous_line_index and @line_index == (@buffer_of_lines.size - 1) - temp_buffer[@previous_line_index] = @line - else - temp_buffer[@line_index] = @line - end @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n") end def insert_text(text) - width = calculate_width(text) - if @cursor == @cursor_max - @line += text + if @buffer_of_lines[@line_index].bytesize == @byte_pointer + @buffer_of_lines[@line_index] += text else - @line = byteinsert(@line, @byte_pointer, text) + @buffer_of_lines[@line_index] = byteinsert(@buffer_of_lines[@line_index], @byte_pointer, text) end @byte_pointer += text.bytesize - @cursor += width - @cursor_max += width + process_auto_indent end def delete_text(start = nil, length = nil) if start.nil? and length.nil? - 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 + if @buffer_of_lines.size == 1 + @buffer_of_lines[@line_index] = '' + @byte_pointer = 0 + elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0 + @buffer_of_lines.pop + @line_index -= 1 + @byte_pointer = 0 + elsif @line_index < (@buffer_of_lines.size - 1) + @buffer_of_lines.delete_at(@line_index) @byte_pointer = 0 - @cursor = 0 - @cursor_max = 0 end elsif not start.nil? and not length.nil? - if @line - before = @line.byteslice(0, start) - after = @line.byteslice(start + length, @line.bytesize) - @line = before + after - @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize - str = @line.byteslice(0, @byte_pointer) - @cursor = calculate_width(str) - @cursor_max = calculate_width(@line) + if current_line + before = current_line.byteslice(0, start) + after = current_line.byteslice(start + length, current_line.bytesize) + set_current_line(before + after) end elsif start.is_a?(Range) range = start first = range.first last = range.last - last = @line.bytesize - 1 if last > @line.bytesize - last += @line.bytesize if last < 0 - first += @line.bytesize if first < 0 + last = current_line.bytesize - 1 if last > current_line.bytesize + last += current_line.bytesize if last < 0 + first += current_line.bytesize if first < 0 range = range.exclude_end? ? first...last : first..last - @line = @line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding) - @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize - str = @line.byteslice(0, @byte_pointer) - @cursor = calculate_width(str) - @cursor_max = calculate_width(@line) + line = current_line.bytes.reject.with_index{ |c, i| range.include?(i) }.map{ |c| c.chr(Encoding::ASCII_8BIT) }.join.force_encoding(@encoding) + set_current_line(line) else - @line = @line.byteslice(0, start) - @byte_pointer = @line.bytesize if @byte_pointer > @line.bytesize - str = @line.byteslice(0, @byte_pointer) - @cursor = calculate_width(str) - @cursor_max = calculate_width(@line) + set_current_line(current_line.byteslice(0, start)) end end def byte_pointer=(val) @byte_pointer = val - str = @line.byteslice(0, @byte_pointer) - @cursor = calculate_width(str) - @cursor_max = calculate_width(@line) end - def whole_lines(index: @line_index, line: @line) - temp_lines = @buffer_of_lines.dup - temp_lines[index] = line - temp_lines + def whole_lines + @buffer_of_lines.dup end def whole_buffer - if @buffer_of_lines.size == 1 and @line.nil? - nil - else - if @previous_line_index - whole_lines(index: @previous_line_index, line: @line).join("\n") - else - whole_lines.join("\n") - end - end + whole_lines.join("\n") end def finished? @@ -1929,7 +1394,6 @@ class Reline::LineEditor def finish @finished = true - @rerender_all = true @config.reset end @@ -1951,40 +1415,36 @@ class Reline::LineEditor end private def key_delete(key) - if @config.editing_mode_is?(:vi_insert, :emacs) + if @config.editing_mode_is?(:vi_insert) ed_delete_next_char(key) + elsif @config.editing_mode_is?(:emacs) + em_delete(key) end end private def key_newline(key) if @is_multiline - if (@buffer_of_lines.size - 1) == @line_index and @line.bytesize == @byte_pointer - @add_newline_to_end_of_buffer = true - end - next_line = @line.byteslice(@byte_pointer, @line.bytesize - @byte_pointer) - cursor_line = @line.byteslice(0, @byte_pointer) + next_line = current_line.byteslice(@byte_pointer, current_line.bytesize - @byte_pointer) + cursor_line = current_line.byteslice(0, @byte_pointer) insert_new_line(cursor_line, next_line) - @cursor = 0 - @check_new_auto_indent = true unless @in_pasting end end + private def completion_journey_up(key) + if not @config.disable_completion and @config.autocompletion + @completion_state = CompletionState::NORMAL + @completion_occurs = move_completed_list(:up) + end + end + alias_method :menu_complete_backward, :completion_journey_up + # Editline:: +ed-unassigned+ This editor command always results in an error. # GNU Readline:: There is no corresponding macro. private def ed_unassigned(key) end # do nothing private def process_insert(force: false) return if @continuous_insertion_buffer.empty? or (@in_pasting and not force) - width = Reline::Unicode.calculate_width(@continuous_insertion_buffer) - bytesize = @continuous_insertion_buffer.bytesize - if @cursor == @cursor_max - @line += @continuous_insertion_buffer - else - @line = byteinsert(@line, @byte_pointer, @continuous_insertion_buffer) - end - @byte_pointer += bytesize - @cursor += width - @cursor_max += width + insert_text(@continuous_insertion_buffer) @continuous_insertion_buffer.clear end @@ -2002,9 +1462,6 @@ class Reline::LineEditor # million. # GNU Readline:: +self-insert+ (a, b, A, 1, !, …) Insert yourself. private def ed_insert(key) - str = nil - width = nil - bytesize = nil if key.instance_of?(String) begin key.encode(Encoding::UTF_8) @@ -2012,7 +1469,6 @@ class Reline::LineEditor return end str = key - bytesize = key.bytesize else begin key.chr.encode(Encoding::UTF_8) @@ -2020,7 +1476,6 @@ class Reline::LineEditor return end str = key.chr - bytesize = 1 end if @in_pasting @continuous_insertion_buffer << str @@ -2028,28 +1483,8 @@ class Reline::LineEditor elsif not @continuous_insertion_buffer.empty? process_insert end - width = Reline::Unicode.get_mbchar_width(str) - if @cursor == @cursor_max - @line += str - else - @line = byteinsert(@line, @byte_pointer, str) - end - last_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) - @byte_pointer += bytesize - last_mbchar = @line.byteslice((@byte_pointer - bytesize - last_byte_size), last_byte_size) - combined_char = last_mbchar + str - if last_byte_size != 0 and combined_char.grapheme_clusters.size == 1 - # combined char - last_mbchar_width = Reline::Unicode.get_mbchar_width(last_mbchar) - combined_char_width = Reline::Unicode.get_mbchar_width(combined_char) - if combined_char_width > last_mbchar_width - width = combined_char_width - last_mbchar_width - else - width = 0 - end - end - @cursor += width - @cursor_max += width + + insert_text(str) end alias_method :ed_digit, :ed_insert alias_method :self_insert, :ed_insert @@ -2071,18 +1506,11 @@ class Reline::LineEditor alias_method :quoted_insert, :ed_quoted_insert private def ed_next_char(key, arg: 1) - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) - if (@byte_pointer < @line.bytesize) - mbchar = @line.byteslice(@byte_pointer, byte_size) - width = Reline::Unicode.get_mbchar_width(mbchar) - @cursor += width if width + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) + if (@byte_pointer < current_line.bytesize) @byte_pointer += byte_size - elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == @line.bytesize and @line_index < @buffer_of_lines.size - 1 - next_line = @buffer_of_lines[@line_index + 1] - @cursor = 0 + elsif @config.editing_mode_is?(:emacs) and @byte_pointer == current_line.bytesize and @line_index < @buffer_of_lines.size - 1 @byte_pointer = 0 - @cursor_max = calculate_width(next_line) - @previous_line_index = @line_index @line_index += 1 end arg -= 1 @@ -2091,19 +1519,12 @@ class Reline::LineEditor alias_method :forward_char, :ed_next_char private def ed_prev_char(key, arg: 1) - if @cursor > 0 - byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) + if @byte_pointer > 0 + byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer) @byte_pointer -= byte_size - mbchar = @line.byteslice(@byte_pointer, byte_size) - width = Reline::Unicode.get_mbchar_width(mbchar) - @cursor -= width - elsif @is_multiline and @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0 - prev_line = @buffer_of_lines[@line_index - 1] - @cursor = calculate_width(prev_line) - @byte_pointer = prev_line.bytesize - @cursor_max = calculate_width(prev_line) - @previous_line_index = @line_index + elsif @config.editing_mode_is?(:emacs) and @byte_pointer == 0 and @line_index > 0 @line_index -= 1 + @byte_pointer = current_line.bytesize end arg -= 1 ed_prev_char(key, arg: arg) if arg > 0 @@ -2111,157 +1532,109 @@ class Reline::LineEditor alias_method :backward_char, :ed_prev_char private def vi_first_print(key) - @byte_pointer, @cursor = Reline::Unicode.vi_first_print(@line) + @byte_pointer, = Reline::Unicode.vi_first_print(current_line) end private def ed_move_to_beg(key) - @byte_pointer = @cursor = 0 + @byte_pointer = 0 end alias_method :beginning_of_line, :ed_move_to_beg + alias_method :vi_zero, :ed_move_to_beg private def ed_move_to_end(key) - @byte_pointer = 0 - @cursor = 0 - byte_size = 0 - while @byte_pointer < @line.bytesize - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) - if byte_size > 0 - mbchar = @line.byteslice(@byte_pointer, byte_size) - @cursor += Reline::Unicode.get_mbchar_width(mbchar) - end - @byte_pointer += byte_size - end + @byte_pointer = current_line.bytesize end alias_method :end_of_line, :ed_move_to_end - private def generate_searcher - Fiber.new do |first_key| - prev_search_key = first_key - search_word = String.new(encoding: @encoding) - multibyte_buf = String.new(encoding: 'ASCII-8BIT') - last_hit = nil - case first_key - when "\C-r".ord - prompt_name = 'reverse-i-search' - when "\C-s".ord - prompt_name = 'i-search' + private def generate_searcher(search_key) + search_word = String.new(encoding: @encoding) + multibyte_buf = String.new(encoding: 'ASCII-8BIT') + hit_pointer = nil + lambda do |key| + search_again = false + case key + when "\C-h".ord, "\C-?".ord + grapheme_clusters = search_word.grapheme_clusters + if grapheme_clusters.size > 0 + grapheme_clusters.pop + search_word = grapheme_clusters.join + end + when "\C-r".ord, "\C-s".ord + search_again = true if search_key == key + search_key = key + else + multibyte_buf << key + if multibyte_buf.dup.force_encoding(@encoding).valid_encoding? + search_word << multibyte_buf.dup.force_encoding(@encoding) + multibyte_buf.clear + end end - loop do - key = Fiber.yield(search_word) - search_again = false - case key - when -1 # determined - Reline.last_incremental_search = search_word - break - when "\C-h".ord, "\C-?".ord - grapheme_clusters = search_word.grapheme_clusters - if grapheme_clusters.size > 0 - grapheme_clusters.pop - search_word = grapheme_clusters.join - end - when "\C-r".ord, "\C-s".ord - search_again = true if prev_search_key == key - prev_search_key = key - else - multibyte_buf << key - if multibyte_buf.dup.force_encoding(@encoding).valid_encoding? - search_word << multibyte_buf.dup.force_encoding(@encoding) - multibyte_buf.clear + hit = nil + if not search_word.empty? and @line_backup_in_history&.include?(search_word) + hit_pointer = Reline::HISTORY.size + hit = @line_backup_in_history + else + if search_again + if search_word.empty? and Reline.last_incremental_search + search_word = Reline.last_incremental_search end - end - hit = nil - if not search_word.empty? and @line_backup_in_history&.include?(search_word) - @history_pointer = nil - hit = @line_backup_in_history - else - if search_again - if search_word.empty? and Reline.last_incremental_search - search_word = Reline.last_incremental_search - end - if @history_pointer - case prev_search_key - when "\C-r".ord - history_pointer_base = 0 - history = Reline::HISTORY[0..(@history_pointer - 1)] - when "\C-s".ord - history_pointer_base = @history_pointer + 1 - history = Reline::HISTORY[(@history_pointer + 1)..-1] - end - else - history_pointer_base = 0 - history = Reline::HISTORY - end - elsif @history_pointer - case prev_search_key + if @history_pointer + case search_key when "\C-r".ord history_pointer_base = 0 - history = Reline::HISTORY[0..@history_pointer] + history = Reline::HISTORY[0..(@history_pointer - 1)] when "\C-s".ord - history_pointer_base = @history_pointer - history = Reline::HISTORY[@history_pointer..-1] + history_pointer_base = @history_pointer + 1 + history = Reline::HISTORY[(@history_pointer + 1)..-1] end else history_pointer_base = 0 history = Reline::HISTORY end - case prev_search_key + elsif @history_pointer + case search_key when "\C-r".ord - hit_index = history.rindex { |item| - item.include?(search_word) - } + history_pointer_base = 0 + history = Reline::HISTORY[0..@history_pointer] when "\C-s".ord - hit_index = history.index { |item| - item.include?(search_word) - } - end - if hit_index - @history_pointer = history_pointer_base + hit_index - hit = Reline::HISTORY[@history_pointer] + history_pointer_base = @history_pointer + history = Reline::HISTORY[@history_pointer..-1] end + else + history_pointer_base = 0 + history = Reline::HISTORY end - case prev_search_key + case search_key when "\C-r".ord - prompt_name = 'reverse-i-search' + hit_index = history.rindex { |item| + item.include?(search_word) + } when "\C-s".ord - prompt_name = 'i-search' + hit_index = history.index { |item| + item.include?(search_word) + } end - if hit - if @is_multiline - @buffer_of_lines = hit.split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = @buffer_of_lines.size - 1 - @line = @buffer_of_lines.last - @byte_pointer = @line.bytesize - @cursor = @cursor_max = calculate_width(@line) - @rerender_all = true - @searching_prompt = "(%s)`%s'" % [prompt_name, search_word] - else - @line = hit - @searching_prompt = "(%s)`%s': %s" % [prompt_name, search_word, hit] - end - last_hit = hit - else - if @is_multiline - @rerender_all = true - @searching_prompt = "(failed %s)`%s'" % [prompt_name, search_word] - else - @searching_prompt = "(failed %s)`%s': %s" % [prompt_name, search_word, last_hit] - end + if hit_index + hit_pointer = history_pointer_base + hit_index + hit = Reline::HISTORY[hit_pointer] end end + case search_key + when "\C-r".ord + prompt_name = 'reverse-i-search' + when "\C-s".ord + prompt_name = 'i-search' + end + prompt_name = "failed #{prompt_name}" unless hit + [search_word, prompt_name, hit_pointer] end end private def incremental_search_history(key) unless @history_pointer - if @is_multiline - @line_backup_in_history = whole_buffer - else - @line_backup_in_history = @line - end + @line_backup_in_history = whole_buffer end - searcher = generate_searcher - searcher.resume(key) + searcher = generate_searcher(key) @searching_prompt = "(reverse-i-search)`': " termination_keys = ["\C-j".ord] termination_keys.concat(@config.isearch_terminators&.chars&.map(&:ord)) if @config.isearch_terminators @@ -2273,67 +1646,41 @@ class Reline::LineEditor else buffer = @line_backup_in_history end - if @is_multiline - @buffer_of_lines = buffer.split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = @buffer_of_lines.size - 1 - @line = @buffer_of_lines.last - @rerender_all = true - else - @line = buffer - end + @buffer_of_lines = buffer.split("\n") + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line_index = @buffer_of_lines.size - 1 @searching_prompt = nil @waiting_proc = nil - @cursor_max = calculate_width(@line) - @cursor = @byte_pointer = 0 - @rerender_all = true - @cached_prompt_list = nil - searcher.resume(-1) + @byte_pointer = 0 when "\C-g".ord - if @is_multiline - @buffer_of_lines = @line_backup_in_history.split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = @buffer_of_lines.size - 1 - @line = @buffer_of_lines.last - @rerender_all = true - else - @line = @line_backup_in_history - end - @history_pointer = nil + @buffer_of_lines = @line_backup_in_history.split("\n") + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line_index = @buffer_of_lines.size - 1 + move_history(nil, line: :end, cursor: :end, save_buffer: false) @searching_prompt = nil @waiting_proc = nil - @line_backup_in_history = nil - @cursor_max = calculate_width(@line) - @cursor = @byte_pointer = 0 - @rerender_all = true + @byte_pointer = 0 else chr = k.is_a?(String) ? k : k.chr(Encoding::ASCII_8BIT) if chr.match?(/[[:print:]]/) or k == "\C-h".ord or k == "\C-?".ord or k == "\C-r".ord or k == "\C-s".ord - searcher.resume(k) + search_word, prompt_name, hit_pointer = searcher.call(k) + Reline.last_incremental_search = search_word + @searching_prompt = "(%s)`%s'" % [prompt_name, search_word] + @searching_prompt += ': ' unless @is_multiline + move_history(hit_pointer, line: :end, cursor: :end, save_buffer: false) if hit_pointer else if @history_pointer line = Reline::HISTORY[@history_pointer] else line = @line_backup_in_history end - if @is_multiline - @line_backup_in_history = whole_buffer - @buffer_of_lines = line.split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = @buffer_of_lines.size - 1 - @line = @buffer_of_lines.last - @rerender_all = true - else - @line_backup_in_history = @line - @line = line - end + @line_backup_in_history = whole_buffer + @buffer_of_lines = line.split("\n") + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line_index = @buffer_of_lines.size - 1 @searching_prompt = nil @waiting_proc = nil - @cursor_max = calculate_width(@line) - @cursor = @byte_pointer = 0 - @rerender_all = true - @cached_prompt_list = nil - searcher.resume(-1) + @byte_pointer = 0 end end } @@ -2349,199 +1696,95 @@ class Reline::LineEditor end alias_method :forward_search_history, :vi_search_next - private def ed_search_prev_history(key, arg: 1) - history = nil - h_pointer = nil - line_no = nil - substr = @line.slice(0, @byte_pointer) - if @history_pointer.nil? - return if not @line.empty? and substr.empty? - history = Reline::HISTORY - elsif @history_pointer.zero? - history = nil - h_pointer = nil - else - history = Reline::HISTORY.slice(0, @history_pointer) - end - return if history.nil? - if @is_multiline - h_pointer = history.rindex { |h| - h.split("\n").each_with_index { |l, i| - if l.start_with?(substr) - line_no = i - break - end - } - not line_no.nil? - } - else - h_pointer = history.rindex { |l| - l.start_with?(substr) - } - end - return if h_pointer.nil? - @history_pointer = h_pointer - if @is_multiline - @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = line_no - @line = @buffer_of_lines[@line_index] - @rerender_all = true - else - @line = Reline::HISTORY[@history_pointer] + private def search_history(prefix, pointer_range) + pointer_range.each do |pointer| + lines = Reline::HISTORY[pointer].split("\n") + lines.each_with_index do |line, index| + return [pointer, index] if line.start_with?(prefix) + end end - @cursor_max = calculate_width(@line) + nil + end + + private def ed_search_prev_history(key, arg: 1) + substr = current_line.byteslice(0, @byte_pointer) + return if @history_pointer == 0 + return if @history_pointer.nil? && substr.empty? && !current_line.empty? + + history_range = 0...(@history_pointer || Reline::HISTORY.size) + h_pointer, line_index = search_history(substr, history_range.reverse_each) + return unless h_pointer + move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer) arg -= 1 ed_search_prev_history(key, arg: arg) if arg > 0 end alias_method :history_search_backward, :ed_search_prev_history private def ed_search_next_history(key, arg: 1) - substr = @line.slice(0, @byte_pointer) - if @history_pointer.nil? - return - elsif @history_pointer == (Reline::HISTORY.size - 1) and not substr.empty? - return - end - history = Reline::HISTORY.slice((@history_pointer + 1)..-1) - h_pointer = nil - line_no = nil - if @is_multiline - h_pointer = history.index { |h| - h.split("\n").each_with_index { |l, i| - if l.start_with?(substr) - line_no = i - break - end - } - not line_no.nil? - } - else - h_pointer = history.index { |l| - l.start_with?(substr) - } - end - h_pointer += @history_pointer + 1 if h_pointer and @history_pointer + substr = current_line.byteslice(0, @byte_pointer) + return if @history_pointer.nil? + + history_range = @history_pointer + 1...Reline::HISTORY.size + h_pointer, line_index = search_history(substr, history_range) return if h_pointer.nil? and not substr.empty? - @history_pointer = h_pointer - if @is_multiline - if @history_pointer.nil? and substr.empty? - @buffer_of_lines = [] - @line_index = 0 - else - @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") - @line_index = line_no - end - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line = @buffer_of_lines[@line_index] - @rerender_all = true - else - if @history_pointer.nil? and substr.empty? - @line = '' - else - @line = Reline::HISTORY[@history_pointer] - end - end - @cursor_max = calculate_width(@line) + + move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer) arg -= 1 ed_search_next_history(key, arg: arg) if arg > 0 end alias_method :history_search_forward, :ed_search_next_history - private def ed_prev_history(key, arg: 1) - if @is_multiline and @line_index > 0 - @previous_line_index = @line_index - @line_index -= 1 - return - end - if Reline::HISTORY.empty? - return + private def move_history(history_pointer, line:, cursor:, save_buffer: true) + history_pointer ||= Reline::HISTORY.size + return if history_pointer < 0 || history_pointer > Reline::HISTORY.size + old_history_pointer = @history_pointer || Reline::HISTORY.size + if old_history_pointer == Reline::HISTORY.size + @line_backup_in_history = save_buffer ? whole_buffer : '' + else + Reline::HISTORY[old_history_pointer] = whole_buffer if save_buffer end - if @history_pointer.nil? - @history_pointer = Reline::HISTORY.size - 1 - if @is_multiline - @line_backup_in_history = whole_buffer - @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = @buffer_of_lines.size - 1 - @line = @buffer_of_lines.last - @rerender_all = true - else - @line_backup_in_history = @line - @line = Reline::HISTORY[@history_pointer] - end - elsif @history_pointer.zero? - return + if history_pointer == Reline::HISTORY.size + buf = @line_backup_in_history + @history_pointer = @line_backup_in_history = nil else - if @is_multiline - Reline::HISTORY[@history_pointer] = whole_buffer - @history_pointer -= 1 - @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = @buffer_of_lines.size - 1 - @line = @buffer_of_lines.last - @rerender_all = true - else - Reline::HISTORY[@history_pointer] = @line - @history_pointer -= 1 - @line = Reline::HISTORY[@history_pointer] - end + buf = Reline::HISTORY[history_pointer] + @history_pointer = history_pointer end - if @config.editing_mode_is?(:emacs, :vi_insert) - @cursor_max = @cursor = calculate_width(@line) - @byte_pointer = @line.bytesize - elsif @config.editing_mode_is?(:vi_command) - @byte_pointer = @cursor = 0 - @cursor_max = calculate_width(@line) + @buffer_of_lines = buf.split("\n") + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line_index = line == :start ? 0 : line == :end ? @buffer_of_lines.size - 1 : line + @byte_pointer = cursor == :start ? 0 : cursor == :end ? current_line.bytesize : cursor + end + + private def ed_prev_history(key, arg: 1) + if @line_index > 0 + cursor = current_byte_pointer_cursor + @line_index -= 1 + calculate_nearest_cursor(cursor) + return end + move_history( + (@history_pointer || Reline::HISTORY.size) - 1, + line: :end, + cursor: @config.editing_mode_is?(:vi_command) ? :start : :end, + ) arg -= 1 ed_prev_history(key, arg: arg) if arg > 0 end alias_method :previous_history, :ed_prev_history private def ed_next_history(key, arg: 1) - if @is_multiline and @line_index < (@buffer_of_lines.size - 1) - @previous_line_index = @line_index + if @line_index < (@buffer_of_lines.size - 1) + cursor = current_byte_pointer_cursor @line_index += 1 + calculate_nearest_cursor(cursor) return end - if @history_pointer.nil? - return - elsif @history_pointer == (Reline::HISTORY.size - 1) - if @is_multiline - @history_pointer = nil - @buffer_of_lines = @line_backup_in_history.split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = 0 - @line = @buffer_of_lines.first - @rerender_all = true - else - @history_pointer = nil - @line = @line_backup_in_history - end - else - if @is_multiline - Reline::HISTORY[@history_pointer] = whole_buffer - @history_pointer += 1 - @buffer_of_lines = Reline::HISTORY[@history_pointer].split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = 0 - @line = @buffer_of_lines.first - @rerender_all = true - else - Reline::HISTORY[@history_pointer] = @line - @history_pointer += 1 - @line = Reline::HISTORY[@history_pointer] - end - end - @line = '' unless @line - if @config.editing_mode_is?(:emacs, :vi_insert) - @cursor_max = @cursor = calculate_width(@line) - @byte_pointer = @line.bytesize - elsif @config.editing_mode_is?(:vi_command) - @byte_pointer = @cursor = 0 - @cursor_max = calculate_width(@line) - end + move_history( + (@history_pointer || Reline::HISTORY.size) + 1, + line: :start, + cursor: @config.editing_mode_is?(:vi_command) ? :start : :end, + ) arg -= 1 ed_next_history(key, arg: arg) if arg > 0 end @@ -2566,40 +1809,29 @@ class Reline::LineEditor end else # should check confirm_multiline_termination to finish? - @previous_line_index = @line_index @line_index = @buffer_of_lines.size - 1 + @byte_pointer = current_line.bytesize finish end end else - if @history_pointer - Reline::HISTORY[@history_pointer] = @line - @history_pointer = nil - end finish end end private def em_delete_prev_char(key, arg: 1) - if @is_multiline and @cursor == 0 and @line_index > 0 - @buffer_of_lines[@line_index] = @line - @cursor = calculate_width(@buffer_of_lines[@line_index - 1]) - @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize - @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index) - @line_index -= 1 - @line = @buffer_of_lines[@line_index] - @cursor_max = calculate_width(@line) - @rerender_all = true - elsif @cursor > 0 - byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) - @byte_pointer -= byte_size - @line, mbchar = byteslice!(@line, @byte_pointer, byte_size) - width = Reline::Unicode.get_mbchar_width(mbchar) - @cursor -= width - @cursor_max -= width + arg.times do + if @byte_pointer == 0 and @line_index > 0 + @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize + @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index) + @line_index -= 1 + elsif @byte_pointer > 0 + byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer) + line, = byteslice!(current_line, @byte_pointer - byte_size, byte_size) + set_current_line(line, @byte_pointer - byte_size) + end end - arg -= 1 - em_delete_prev_char(key, arg: arg) if arg > 0 + process_auto_indent end alias_method :backward_delete_char, :em_delete_prev_char @@ -2609,23 +1841,23 @@ class Reline::LineEditor # the line. With a negative numeric argument, kill backward # from the cursor to the beginning of the current line. private def ed_kill_line(key) - if @line.bytesize > @byte_pointer - @line, deleted = byteslice!(@line, @byte_pointer, @line.bytesize - @byte_pointer) - @byte_pointer = @line.bytesize - @cursor = @cursor_max = calculate_width(@line) + if current_line.bytesize > @byte_pointer + line, deleted = byteslice!(current_line, @byte_pointer, current_line.bytesize - @byte_pointer) + set_current_line(line, line.bytesize) @kill_ring.append(deleted) - elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1 - @cursor = calculate_width(@line) - @byte_pointer = @line.bytesize - @line += @buffer_of_lines.delete_at(@line_index + 1) - @cursor_max = calculate_width(@line) - @buffer_of_lines[@line_index] = @line - @rerender_all = true - @rest_height += 1 + elsif @byte_pointer == current_line.bytesize and @buffer_of_lines.size > @line_index + 1 + set_current_line(current_line + @buffer_of_lines.delete_at(@line_index + 1), current_line.bytesize) end end alias_method :kill_line, :ed_kill_line + # Editline:: +vi_change_to_eol+ (vi command: +C+) + Kill and change from the cursor to the end of the line. + private def vi_change_to_eol(key) + ed_kill_line(key) + + @config.editing_mode = :vi_insert + end + # Editline:: +vi-kill-line-prev+ (vi: +Ctrl-U+) Delete the string from the # beginning of the edit buffer to the cursor and save it to the # cut buffer. @@ -2633,11 +1865,9 @@ class Reline::LineEditor # to the beginning of the current line. private def vi_kill_line_prev(key) if @byte_pointer > 0 - @line, deleted = byteslice!(@line, 0, @byte_pointer) - @byte_pointer = 0 + line, deleted = byteslice!(current_line, 0, @byte_pointer) + set_current_line(line, 0) @kill_ring.append(deleted, true) - @cursor_max = calculate_width(@line) - @cursor = 0 end end alias_method :unix_line_discard, :vi_kill_line_prev @@ -2647,47 +1877,32 @@ class Reline::LineEditor # GNU Readline:: +kill-whole-line+ (not bound) Kill all characters on the # current line, no matter where point is. private def em_kill_line(key) - if @line.size > 0 - @kill_ring.append(@line.dup, true) - @line.clear - @byte_pointer = 0 - @cursor_max = 0 - @cursor = 0 + if current_line.size > 0 + @kill_ring.append(current_line.dup, true) + set_current_line('', 0) end end alias_method :kill_whole_line, :em_kill_line private def em_delete(key) - if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1) - @line = nil - if @buffer_of_lines.size > 1 - scroll_down(@highest_in_all - @first_line_started_from) - end - Reline::IOGate.move_cursor_column(0) + if current_line.empty? and @buffer_of_lines.size == 1 and key == "\C-d".ord @eof = true finish - elsif @byte_pointer < @line.bytesize - splitted_last = @line.byteslice(@byte_pointer, @line.bytesize) + elsif @byte_pointer < current_line.bytesize + splitted_last = current_line.byteslice(@byte_pointer, current_line.bytesize) mbchar = splitted_last.grapheme_clusters.first - width = Reline::Unicode.get_mbchar_width(mbchar) - @cursor_max -= width - @line, = byteslice!(@line, @byte_pointer, mbchar.bytesize) - elsif @is_multiline and @byte_pointer == @line.bytesize and @buffer_of_lines.size > @line_index + 1 - @cursor = calculate_width(@line) - @byte_pointer = @line.bytesize - @line += @buffer_of_lines.delete_at(@line_index + 1) - @cursor_max = calculate_width(@line) - @buffer_of_lines[@line_index] = @line - @rerender_all = true - @rest_height += 1 + line, = byteslice!(current_line, @byte_pointer, mbchar.bytesize) + set_current_line(line) + elsif @byte_pointer == current_line.bytesize and @buffer_of_lines.size > @line_index + 1 + set_current_line(current_line + @buffer_of_lines.delete_at(@line_index + 1), current_line.bytesize) end end alias_method :delete_char, :em_delete private def em_delete_or_list(key) - if @line.empty? or @byte_pointer < @line.bytesize + if current_line.empty? or @byte_pointer < current_line.bytesize em_delete(key) - else # show completed list + elsif !@config.autocompletion # show completed list result = call_completion_proc if result.is_a?(Array) complete(result, true) @@ -2698,162 +1913,136 @@ class Reline::LineEditor private def em_yank(key) yanked = @kill_ring.yank - if yanked - @line = byteinsert(@line, @byte_pointer, yanked) - yanked_width = calculate_width(yanked) - @cursor += yanked_width - @cursor_max += yanked_width - @byte_pointer += yanked.bytesize - end + insert_text(yanked) if yanked end alias_method :yank, :em_yank private def em_yank_pop(key) yanked, prev_yank = @kill_ring.yank_pop if yanked - prev_yank_width = calculate_width(prev_yank) - @cursor -= prev_yank_width - @cursor_max -= prev_yank_width - @byte_pointer -= prev_yank.bytesize - @line, = byteslice!(@line, @byte_pointer, prev_yank.bytesize) - @line = byteinsert(@line, @byte_pointer, yanked) - yanked_width = calculate_width(yanked) - @cursor += yanked_width - @cursor_max += yanked_width - @byte_pointer += yanked.bytesize + line, = byteslice!(current_line, @byte_pointer - prev_yank.bytesize, prev_yank.bytesize) + set_current_line(line, @byte_pointer - prev_yank.bytesize) + insert_text(yanked) end end alias_method :yank_pop, :em_yank_pop private def ed_clear_screen(key) - @cleared = true + Reline::IOGate.clear_screen + @screen_size = Reline::IOGate.get_screen_size + @rendered_screen.lines = [] + @rendered_screen.base_y = 0 + @rendered_screen.cursor_y = 0 end alias_method :clear_screen, :ed_clear_screen private def em_next_word(key) - if @line.bytesize > @byte_pointer - byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.em_forward_word(current_line, @byte_pointer) @byte_pointer += byte_size - @cursor += width end end alias_method :forward_word, :em_next_word private def ed_prev_word(key) if @byte_pointer > 0 - byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer) + byte_size, _ = Reline::Unicode.em_backward_word(current_line, @byte_pointer) @byte_pointer -= byte_size - @cursor -= width end end alias_method :backward_word, :ed_prev_word private def em_delete_next_word(key) - if @line.bytesize > @byte_pointer - byte_size, width = Reline::Unicode.em_forward_word(@line, @byte_pointer) - @line, word = byteslice!(@line, @byte_pointer, byte_size) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.em_forward_word(current_line, @byte_pointer) + line, word = byteslice!(current_line, @byte_pointer, byte_size) + set_current_line(line) @kill_ring.append(word) - @cursor_max -= width end end + alias_method :kill_word, :em_delete_next_word private def ed_delete_prev_word(key) if @byte_pointer > 0 - byte_size, width = Reline::Unicode.em_backward_word(@line, @byte_pointer) - @line, word = byteslice!(@line, @byte_pointer - byte_size, byte_size) + byte_size, _ = Reline::Unicode.em_backward_word(current_line, @byte_pointer) + line, word = byteslice!(current_line, @byte_pointer - byte_size, byte_size) + set_current_line(line, @byte_pointer - byte_size) @kill_ring.append(word, true) - @byte_pointer -= byte_size - @cursor -= width - @cursor_max -= width end end + alias_method :backward_kill_word, :ed_delete_prev_word private def ed_transpose_chars(key) if @byte_pointer > 0 - if @cursor_max > @cursor - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) - mbchar = @line.byteslice(@byte_pointer, byte_size) - width = Reline::Unicode.get_mbchar_width(mbchar) - @cursor += width + if @byte_pointer < current_line.bytesize + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) @byte_pointer += byte_size end - back1_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) + back1_byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer) if (@byte_pointer - back1_byte_size) > 0 - back2_byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer - back1_byte_size) + back2_byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer - back1_byte_size) back2_pointer = @byte_pointer - back1_byte_size - back2_byte_size - @line, back2_mbchar = byteslice!(@line, back2_pointer, back2_byte_size) - @line = byteinsert(@line, @byte_pointer - back2_byte_size, back2_mbchar) + line, back2_mbchar = byteslice!(current_line, back2_pointer, back2_byte_size) + set_current_line(byteinsert(line, @byte_pointer - back2_byte_size, back2_mbchar)) end end end alias_method :transpose_chars, :ed_transpose_chars private def ed_transpose_words(key) - left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(@line, @byte_pointer) - before = @line.byteslice(0, left_word_start) - left_word = @line.byteslice(left_word_start, middle_start - left_word_start) - middle = @line.byteslice(middle_start, right_word_start - middle_start) - right_word = @line.byteslice(right_word_start, after_start - right_word_start) - after = @line.byteslice(after_start, @line.bytesize - after_start) + left_word_start, middle_start, right_word_start, after_start = Reline::Unicode.ed_transpose_words(current_line, @byte_pointer) + before = current_line.byteslice(0, left_word_start) + left_word = current_line.byteslice(left_word_start, middle_start - left_word_start) + middle = current_line.byteslice(middle_start, right_word_start - middle_start) + right_word = current_line.byteslice(right_word_start, after_start - right_word_start) + after = current_line.byteslice(after_start, current_line.bytesize - after_start) return if left_word.empty? or right_word.empty? - @line = before + right_word + middle + left_word + after from_head_to_left_word = before + right_word + middle + left_word - @byte_pointer = from_head_to_left_word.bytesize - @cursor = calculate_width(from_head_to_left_word) + set_current_line(from_head_to_left_word + after, from_head_to_left_word.bytesize) end alias_method :transpose_words, :ed_transpose_words private def em_capitol_case(key) - if @line.bytesize > @byte_pointer - byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(@line, @byte_pointer) - before = @line.byteslice(0, @byte_pointer) - after = @line.byteslice((@byte_pointer + byte_size)..-1) - @line = before + new_str + after - @byte_pointer += new_str.bytesize - @cursor += calculate_width(new_str) + if current_line.bytesize > @byte_pointer + byte_size, _, new_str = Reline::Unicode.em_forward_word_with_capitalization(current_line, @byte_pointer) + before = current_line.byteslice(0, @byte_pointer) + after = current_line.byteslice((@byte_pointer + byte_size)..-1) + set_current_line(before + new_str + after, @byte_pointer + new_str.bytesize) end end alias_method :capitalize_word, :em_capitol_case private def em_lower_case(key) - if @line.bytesize > @byte_pointer - byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer) - part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar| + if current_line.bytesize > @byte_pointer + byte_size, = Reline::Unicode.em_forward_word(current_line, @byte_pointer) + part = current_line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar| mbchar =~ /[A-Z]/ ? mbchar.downcase : mbchar }.join - rest = @line.byteslice((@byte_pointer + byte_size)..-1) - @line = @line.byteslice(0, @byte_pointer) + part - @byte_pointer = @line.bytesize - @cursor = calculate_width(@line) - @cursor_max = @cursor + calculate_width(rest) - @line += rest + rest = current_line.byteslice((@byte_pointer + byte_size)..-1) + line = current_line.byteslice(0, @byte_pointer) + part + set_current_line(line + rest, line.bytesize) end end alias_method :downcase_word, :em_lower_case private def em_upper_case(key) - if @line.bytesize > @byte_pointer - byte_size, = Reline::Unicode.em_forward_word(@line, @byte_pointer) - part = @line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar| + if current_line.bytesize > @byte_pointer + byte_size, = Reline::Unicode.em_forward_word(current_line, @byte_pointer) + part = current_line.byteslice(@byte_pointer, byte_size).grapheme_clusters.map { |mbchar| mbchar =~ /[a-z]/ ? mbchar.upcase : mbchar }.join - rest = @line.byteslice((@byte_pointer + byte_size)..-1) - @line = @line.byteslice(0, @byte_pointer) + part - @byte_pointer = @line.bytesize - @cursor = calculate_width(@line) - @cursor_max = @cursor + calculate_width(rest) - @line += rest + rest = current_line.byteslice((@byte_pointer + byte_size)..-1) + line = current_line.byteslice(0, @byte_pointer) + part + set_current_line(line + rest, line.bytesize) end end alias_method :upcase_word, :em_upper_case private def em_kill_region(key) if @byte_pointer > 0 - byte_size, width = Reline::Unicode.em_big_backward_word(@line, @byte_pointer) - @line, deleted = byteslice!(@line, @byte_pointer - byte_size, byte_size) - @byte_pointer -= byte_size - @cursor -= width - @cursor_max -= width + byte_size, _ = Reline::Unicode.em_big_backward_word(current_line, @byte_pointer) + line, deleted = byteslice!(current_line, @byte_pointer - byte_size, byte_size) + set_current_line(line, @byte_pointer - byte_size) @kill_ring.append(deleted, true) end end @@ -2881,10 +2070,9 @@ class Reline::LineEditor alias_method :vi_movement_mode, :vi_command_mode private def vi_next_word(key, arg: 1) - if @line.bytesize > @byte_pointer - byte_size, width = Reline::Unicode.vi_forward_word(@line, @byte_pointer, @drop_terminate_spaces) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.vi_forward_word(current_line, @byte_pointer, @drop_terminate_spaces) @byte_pointer += byte_size - @cursor += width end arg -= 1 vi_next_word(key, arg: arg) if arg > 0 @@ -2892,38 +2080,32 @@ class Reline::LineEditor private def vi_prev_word(key, arg: 1) if @byte_pointer > 0 - byte_size, width = Reline::Unicode.vi_backward_word(@line, @byte_pointer) + byte_size, _ = Reline::Unicode.vi_backward_word(current_line, @byte_pointer) @byte_pointer -= byte_size - @cursor -= width end arg -= 1 vi_prev_word(key, arg: arg) if arg > 0 end private def vi_end_word(key, arg: 1, inclusive: false) - if @line.bytesize > @byte_pointer - byte_size, width = Reline::Unicode.vi_forward_end_word(@line, @byte_pointer) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.vi_forward_end_word(current_line, @byte_pointer) @byte_pointer += byte_size - @cursor += width end arg -= 1 if inclusive and arg.zero? - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) if byte_size > 0 - c = @line.byteslice(@byte_pointer, byte_size) - width = Reline::Unicode.get_mbchar_width(c) @byte_pointer += byte_size - @cursor += width end end vi_end_word(key, arg: arg) if arg > 0 end private def vi_next_big_word(key, arg: 1) - if @line.bytesize > @byte_pointer - byte_size, width = Reline::Unicode.vi_big_forward_word(@line, @byte_pointer) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.vi_big_forward_word(current_line, @byte_pointer) @byte_pointer += byte_size - @cursor += width end arg -= 1 vi_next_big_word(key, arg: arg) if arg > 0 @@ -2931,50 +2113,39 @@ class Reline::LineEditor private def vi_prev_big_word(key, arg: 1) if @byte_pointer > 0 - byte_size, width = Reline::Unicode.vi_big_backward_word(@line, @byte_pointer) + byte_size, _ = Reline::Unicode.vi_big_backward_word(current_line, @byte_pointer) @byte_pointer -= byte_size - @cursor -= width end arg -= 1 vi_prev_big_word(key, arg: arg) if arg > 0 end private def vi_end_big_word(key, arg: 1, inclusive: false) - if @line.bytesize > @byte_pointer - byte_size, width = Reline::Unicode.vi_big_forward_end_word(@line, @byte_pointer) + if current_line.bytesize > @byte_pointer + byte_size, _ = Reline::Unicode.vi_big_forward_end_word(current_line, @byte_pointer) @byte_pointer += byte_size - @cursor += width end arg -= 1 if inclusive and arg.zero? - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) if byte_size > 0 - c = @line.byteslice(@byte_pointer, byte_size) - width = Reline::Unicode.get_mbchar_width(c) @byte_pointer += byte_size - @cursor += width end end vi_end_big_word(key, arg: arg) if arg > 0 end private def vi_delete_prev_char(key) - if @is_multiline and @cursor == 0 and @line_index > 0 - @buffer_of_lines[@line_index] = @line - @cursor = calculate_width(@buffer_of_lines[@line_index - 1]) + if @byte_pointer == 0 and @line_index > 0 @byte_pointer = @buffer_of_lines[@line_index - 1].bytesize @buffer_of_lines[@line_index - 1] += @buffer_of_lines.delete_at(@line_index) @line_index -= 1 - @line = @buffer_of_lines[@line_index] - @cursor_max = calculate_width(@line) - @rerender_all = true - elsif @cursor > 0 - byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) + process_auto_indent cursor_dependent: false + elsif @byte_pointer > 0 + byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer) @byte_pointer -= byte_size - @line, mbchar = byteslice!(@line, @byte_pointer, byte_size) - width = Reline::Unicode.get_mbchar_width(mbchar) - @cursor -= width - @cursor_max -= width + line, _ = byteslice!(current_line, @byte_pointer, byte_size) + set_current_line(line) end end @@ -2989,78 +2160,81 @@ class Reline::LineEditor end private def ed_delete_prev_char(key, arg: 1) - deleted = '' + deleted = +'' arg.times do - if @cursor > 0 - byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) + if @byte_pointer > 0 + byte_size = Reline::Unicode.get_prev_mbchar_size(current_line, @byte_pointer) @byte_pointer -= byte_size - @line, mbchar = byteslice!(@line, @byte_pointer, byte_size) + line, mbchar = byteslice!(current_line, @byte_pointer, byte_size) + set_current_line(line) deleted.prepend(mbchar) - width = Reline::Unicode.get_mbchar_width(mbchar) - @cursor -= width - @cursor_max -= width end end copy_for_vi(deleted) end - private def vi_zero(key) - @byte_pointer = 0 - @cursor = 0 - end - - private def vi_change_meta(key, arg: 1) - @drop_terminate_spaces = true - @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff| - if byte_pointer_diff > 0 - @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff) - elsif byte_pointer_diff < 0 - @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff) - end - copy_for_vi(cut) - @cursor += cursor_diff if cursor_diff < 0 - @cursor_max -= cursor_diff.abs - @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0 - @config.editing_mode = :vi_insert - @drop_terminate_spaces = false - } - @waiting_operator_vi_arg = arg + private def vi_change_meta(key, arg: nil) + if @vi_waiting_operator + set_current_line('', 0) if @vi_waiting_operator == :vi_change_meta_confirm && arg.nil? + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + else + @drop_terminate_spaces = true + @vi_waiting_operator = :vi_change_meta_confirm + @vi_waiting_operator_arg = arg || 1 + end end - private def vi_delete_meta(key, arg: 1) - @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff| - if byte_pointer_diff > 0 - @line, cut = byteslice!(@line, @byte_pointer, byte_pointer_diff) - elsif byte_pointer_diff < 0 - @line, cut = byteslice!(@line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff) - end - copy_for_vi(cut) - @cursor += cursor_diff if cursor_diff < 0 - @cursor_max -= cursor_diff.abs - @byte_pointer += byte_pointer_diff if byte_pointer_diff < 0 - } - @waiting_operator_vi_arg = arg + private def vi_change_meta_confirm(byte_pointer_diff) + vi_delete_meta_confirm(byte_pointer_diff) + @config.editing_mode = :vi_insert + @drop_terminate_spaces = false end - private def vi_yank(key, arg: 1) - @waiting_operator_proc = proc { |cursor_diff, byte_pointer_diff| - if byte_pointer_diff > 0 - cut = @line.byteslice(@byte_pointer, byte_pointer_diff) - elsif byte_pointer_diff < 0 - cut = @line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff) - end - copy_for_vi(cut) - } - @waiting_operator_vi_arg = arg + private def vi_delete_meta(key, arg: nil) + if @vi_waiting_operator + set_current_line('', 0) if @vi_waiting_operator == :vi_delete_meta_confirm && arg.nil? + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + else + @vi_waiting_operator = :vi_delete_meta_confirm + @vi_waiting_operator_arg = arg || 1 + end + end + + private def vi_delete_meta_confirm(byte_pointer_diff) + if byte_pointer_diff > 0 + line, cut = byteslice!(current_line, @byte_pointer, byte_pointer_diff) + elsif byte_pointer_diff < 0 + line, cut = byteslice!(current_line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff) + end + copy_for_vi(cut) + set_current_line(line || '', @byte_pointer + (byte_pointer_diff < 0 ? byte_pointer_diff : 0)) + end + + private def vi_yank(key, arg: nil) + if @vi_waiting_operator + copy_for_vi(current_line) if @vi_waiting_operator == :vi_yank_confirm && arg.nil? + @vi_waiting_operator = nil + @vi_waiting_operator_arg = nil + else + @vi_waiting_operator = :vi_yank_confirm + @vi_waiting_operator_arg = arg || 1 + end + end + + private def vi_yank_confirm(byte_pointer_diff) + if byte_pointer_diff > 0 + cut = current_line.byteslice(@byte_pointer, byte_pointer_diff) + elsif byte_pointer_diff < 0 + cut = current_line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff) + end + copy_for_vi(cut) end private def vi_list_or_eof(key) - if (not @is_multiline and @line.empty?) or (@is_multiline and @line.empty? and @buffer_of_lines.size == 1) - @line = nil - if @buffer_of_lines.size > 1 - scroll_down(@highest_in_all - @first_line_started_from) - end - Reline::IOGate.move_cursor_column(0) + if current_line.empty? and @buffer_of_lines.size == 1 + set_current_line('', 0) @eof = true finish else @@ -3071,18 +2245,15 @@ class Reline::LineEditor alias_method :vi_eof_maybe, :vi_list_or_eof private def ed_delete_next_char(key, arg: 1) - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) - unless @line.empty? || byte_size == 0 - @line, mbchar = byteslice!(@line, @byte_pointer, byte_size) + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) + unless current_line.empty? || byte_size == 0 + line, mbchar = byteslice!(current_line, @byte_pointer, byte_size) copy_for_vi(mbchar) - width = Reline::Unicode.get_mbchar_width(mbchar) - @cursor_max -= width - if @cursor > 0 and @cursor >= @cursor_max - byte_size = Reline::Unicode.get_prev_mbchar_size(@line, @byte_pointer) - mbchar = @line.byteslice(@byte_pointer - byte_size, byte_size) - width = Reline::Unicode.get_mbchar_width(mbchar) - @byte_pointer -= byte_size - @cursor -= width + if @byte_pointer > 0 && current_line.bytesize == @byte_pointer + byte_size + byte_size = Reline::Unicode.get_prev_mbchar_size(line, @byte_pointer) + set_current_line(line, @byte_pointer - byte_size) + else + set_current_line(line, @byte_pointer) end end arg -= 1 @@ -3093,54 +2264,25 @@ class Reline::LineEditor if Reline::HISTORY.empty? return end - if @history_pointer.nil? - @history_pointer = 0 - @line_backup_in_history = @line - @line = Reline::HISTORY[@history_pointer] - @cursor_max = calculate_width(@line) - @cursor = 0 - @byte_pointer = 0 - elsif @history_pointer.zero? - return - else - Reline::HISTORY[@history_pointer] = @line - @history_pointer = 0 - @line = Reline::HISTORY[@history_pointer] - @cursor_max = calculate_width(@line) - @cursor = 0 - @byte_pointer = 0 - end + move_history(0, line: :start, cursor: :start) end private def vi_histedit(key) path = Tempfile.open { |fp| - if @is_multiline - fp.write whole_lines.join("\n") - else - fp.write @line - end + fp.write whole_lines.join("\n") fp.path } system("#{ENV['EDITOR']} #{path}") - if @is_multiline - @buffer_of_lines = File.read(path).split("\n") - @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? - @line_index = 0 - @line = @buffer_of_lines[@line_index] - @rerender_all = true - else - @line = File.read(path) - end + @buffer_of_lines = File.read(path).split("\n") + @buffer_of_lines = [String.new(encoding: @encoding)] if @buffer_of_lines.empty? + @line_index = 0 finish end private def vi_paste_prev(key, arg: 1) if @vi_clipboard.size > 0 - @line = byteinsert(@line, @byte_pointer, @vi_clipboard) - @cursor_max += calculate_width(@vi_clipboard) cursor_point = @vi_clipboard.grapheme_clusters[0..-2].join - @cursor += calculate_width(cursor_point) - @byte_pointer += cursor_point.bytesize + set_current_line(byteinsert(current_line, @byte_pointer, @vi_clipboard), @byte_pointer + cursor_point.bytesize) end arg -= 1 vi_paste_prev(key, arg: arg) if arg > 0 @@ -3148,11 +2290,9 @@ class Reline::LineEditor private def vi_paste_next(key, arg: 1) if @vi_clipboard.size > 0 - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) - @line = byteinsert(@line, @byte_pointer + byte_size, @vi_clipboard) - @cursor_max += calculate_width(@vi_clipboard) - @cursor += calculate_width(@vi_clipboard) - @byte_pointer += @vi_clipboard.bytesize + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) + line = byteinsert(current_line, @byte_pointer + byte_size, @vi_clipboard) + set_current_line(line, @byte_pointer + @vi_clipboard.bytesize) end arg -= 1 vi_paste_next(key, arg: arg) if arg > 0 @@ -3176,43 +2316,33 @@ class Reline::LineEditor end private def vi_to_column(key, arg: 0) - @byte_pointer, @cursor = @line.grapheme_clusters.inject([0, 0]) { |total, gc| - # total has [byte_size, cursor] + # Implementing behavior of vi, not Readline's vi-mode. + @byte_pointer, = current_line.grapheme_clusters.inject([0, 0]) { |(total_byte_size, total_width), gc| mbchar_width = Reline::Unicode.get_mbchar_width(gc) - if (total.last + mbchar_width) >= arg - break total - elsif (total.last + mbchar_width) >= @cursor_max - break total - else - total = [total.first + gc.bytesize, total.last + mbchar_width] - total - end + break [total_byte_size, total_width] if (total_width + mbchar_width) >= arg + [total_byte_size + gc.bytesize, total_width + mbchar_width] } end private def vi_replace_char(key, arg: 1) @waiting_proc = ->(k) { if arg == 1 - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) - before = @line.byteslice(0, @byte_pointer) + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) + before = current_line.byteslice(0, @byte_pointer) remaining_point = @byte_pointer + byte_size - after = @line.byteslice(remaining_point, @line.bytesize - remaining_point) - @line = before + k.chr + after - @cursor_max = calculate_width(@line) + after = current_line.byteslice(remaining_point, current_line.bytesize - remaining_point) + set_current_line(before + k.chr + after) @waiting_proc = nil elsif arg > 1 byte_size = 0 arg.times do - byte_size += Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer + byte_size) + byte_size += Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer + byte_size) end - before = @line.byteslice(0, @byte_pointer) + before = current_line.byteslice(0, @byte_pointer) remaining_point = @byte_pointer + byte_size - after = @line.byteslice(remaining_point, @line.bytesize - remaining_point) + after = current_line.byteslice(remaining_point, current_line.bytesize - remaining_point) replaced = k.chr * arg - @line = before + replaced + after - @byte_pointer += replaced.bytesize - @cursor += calculate_width(replaced) - @cursor_max = calculate_width(@line) + set_current_line(before + replaced + after, @byte_pointer + replaced.bytesize) @waiting_proc = nil end } @@ -3235,7 +2365,7 @@ class Reline::LineEditor prev_total = nil total = nil found = false - @line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar| + current_line.byteslice(@byte_pointer..-1).grapheme_clusters.each do |mbchar| # total has [byte_size, cursor] unless total # skip cursor point @@ -3255,21 +2385,16 @@ class Reline::LineEditor end end if not need_prev_char and found and total - byte_size, width = total + byte_size, _ = total @byte_pointer += byte_size - @cursor += width elsif need_prev_char and found and prev_total - byte_size, width = prev_total + byte_size, _ = prev_total @byte_pointer += byte_size - @cursor += width end if inclusive - byte_size = Reline::Unicode.get_next_mbchar_size(@line, @byte_pointer) + byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) if byte_size > 0 - c = @line.byteslice(@byte_pointer, byte_size) - width = Reline::Unicode.get_mbchar_width(c) @byte_pointer += byte_size - @cursor += width end end @waiting_proc = nil @@ -3292,7 +2417,7 @@ class Reline::LineEditor prev_total = nil total = nil found = false - @line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar| + current_line.byteslice(0..@byte_pointer).grapheme_clusters.reverse_each do |mbchar| # total has [byte_size, cursor] unless total # skip cursor point @@ -3312,26 +2437,19 @@ class Reline::LineEditor end end if not need_next_char and found and total - byte_size, width = total + byte_size, _ = total @byte_pointer -= byte_size - @cursor -= width elsif need_next_char and found and prev_total - byte_size, width = prev_total + byte_size, _ = prev_total @byte_pointer -= byte_size - @cursor -= width end @waiting_proc = nil end private def vi_join_lines(key, arg: 1) - if @is_multiline and @buffer_of_lines.size > @line_index + 1 - @cursor = calculate_width(@line) - @byte_pointer = @line.bytesize - @line += ' ' + @buffer_of_lines.delete_at(@line_index + 1).lstrip - @cursor_max = calculate_width(@line) - @buffer_of_lines[@line_index] = @line - @rerender_all = true - @rest_height += 1 + if @buffer_of_lines.size > @line_index + 1 + next_line = @buffer_of_lines.delete_at(@line_index + 1).lstrip + set_current_line(current_line + ' ' + next_line, current_line.bytesize) end arg -= 1 vi_join_lines(key, arg: arg) if arg > 0 @@ -3345,11 +2463,16 @@ class Reline::LineEditor private def em_exchange_mark(key) return unless @mark_pointer new_pointer = [@byte_pointer, @line_index] - @previous_line_index = @line_index @byte_pointer, @line_index = @mark_pointer - @cursor = calculate_width(@line.byteslice(0, @byte_pointer)) - @cursor_max = calculate_width(@line) @mark_pointer = new_pointer end alias_method :exchange_point_and_mark, :em_exchange_mark + + private def emacs_editing_mode(key) + @config.editing_mode = :emacs + end + + private def vi_editing_mode(key) + @config.editing_mode = :vi_insert + end end diff --git a/lib/reline/reline.gemspec b/lib/reline/reline.gemspec index 7bf1f8758b..dfaf966728 100644 --- a/lib/reline/reline.gemspec +++ b/lib/reline/reline.gemspec @@ -18,6 +18,11 @@ Gem::Specification.new do |spec| spec.files = Dir['BSDL', 'COPYING', 'README.md', 'license_of_rb-readline', 'lib/**/*'] spec.require_paths = ['lib'] + spec.metadata = { + "bug_tracker_uri" => "https://github.com/ruby/reline/issues", + "changelog_uri" => "https://github.com/ruby/reline/releases", + "source_code_uri" => "https://github.com/ruby/reline" + } spec.required_ruby_version = Gem::Requirement.new('>= 2.6') diff --git a/lib/reline/terminfo.rb b/lib/reline/terminfo.rb index f53642b919..6885a0c6be 100644 --- a/lib/reline/terminfo.rb +++ b/lib/reline/terminfo.rb @@ -31,21 +31,7 @@ module Reline::Terminfo @curses_dl = false def self.curses_dl return @curses_dl unless @curses_dl == false - if RUBY_VERSION >= '3.0.0' - # Gem module isn't defined in test-all of the Ruby repository, and - # Fiddle in Ruby 3.0.0 or later supports Fiddle::TYPE_VARIADIC. - fiddle_supports_variadic = true - elsif Fiddle.const_defined?(:VERSION,false) and Gem::Version.create(Fiddle::VERSION) >= Gem::Version.create('1.0.1') - # Fiddle::TYPE_VARIADIC is supported from Fiddle 1.0.1. - fiddle_supports_variadic = true - else - fiddle_supports_variadic = false - end - if fiddle_supports_variadic and not Fiddle.const_defined?(:TYPE_VARIADIC) - # If the libffi version is not 3.0.5 or higher, there isn't TYPE_VARIADIC. - fiddle_supports_variadic = false - end - if fiddle_supports_variadic + if Fiddle.const_defined?(:TYPE_VARIADIC) curses_dl_files.each do |curses_name| result = Fiddle::Handle.new(curses_name) rescue Fiddle::DLError @@ -94,23 +80,11 @@ module Reline::Terminfo def self.setupterm(term, fildes) errret_int = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT) ret = @setupterm.(term, fildes, errret_int) - errret = errret_int[0, Fiddle::SIZEOF_INT].unpack1('i') case ret when 0 # OK - 0 + @term_supported = true when -1 # ERR - case errret - when 1 - raise TerminfoError.new('The terminal is hardcopy, cannot be used for curses applications.') - when 0 - raise TerminfoError.new('The terminal could not be found, or that it is a generic type, having too little information for curses applications to run.') - when -1 - raise TerminfoError.new('The terminfo database could not be found.') - else # unknown - -1 - end - else # unknown - -2 + @term_supported = false end end @@ -162,9 +136,14 @@ module Reline::Terminfo num end + # NOTE: This means Fiddle and curses are enabled. def self.enabled? true end + + def self.term_supported? + @term_supported + end end if Reline::Terminfo.curses_dl module Reline::Terminfo diff --git a/lib/reline/unicode.rb b/lib/reline/unicode.rb index 6000c9f82a..7f94e95287 100644 --- a/lib/reline/unicode.rb +++ b/lib/reline/unicode.rb @@ -38,33 +38,8 @@ class Reline::Unicode NON_PRINTING_START = "\1" NON_PRINTING_END = "\2" CSI_REGEXP = /\e\[[\d;]*[ABCDEFGHJKSTfminsuhl]/ - OSC_REGEXP = /\e\]\d+(?:;[^;]+)*\a/ + OSC_REGEXP = /\e\]\d+(?:;[^;\a\e]+)*(?:\a|\e\\)/ WIDTH_SCANNER = /\G(?:(#{NON_PRINTING_START})|(#{NON_PRINTING_END})|(#{CSI_REGEXP})|(#{OSC_REGEXP})|(\X))/o - NON_PRINTING_START_INDEX = 0 - NON_PRINTING_END_INDEX = 1 - CSI_REGEXP_INDEX = 2 - OSC_REGEXP_INDEX = 3 - GRAPHEME_CLUSTER_INDEX = 4 - - def self.get_mbchar_byte_size_by_first_char(c) - # Checks UTF-8 character byte size - case c.ord - # 0b0xxxxxxx - when ->(code) { (code ^ 0b10000000).allbits?(0b10000000) } then 1 - # 0b110xxxxx - when ->(code) { (code ^ 0b00100000).allbits?(0b11100000) } then 2 - # 0b1110xxxx - when ->(code) { (code ^ 0b00010000).allbits?(0b11110000) } then 3 - # 0b11110xxx - when ->(code) { (code ^ 0b00001000).allbits?(0b11111000) } then 4 - # 0b111110xx - when ->(code) { (code ^ 0b00000100).allbits?(0b11111100) } then 5 - # 0b1111110x - when ->(code) { (code ^ 0b00000010).allbits?(0b11111110) } then 6 - # successor of mbchar - else 0 - end - end def self.escape_for_print(str) str.chars.map! { |gr| @@ -132,15 +107,14 @@ class Reline::Unicode width = 0 rest = str.encode(Encoding::UTF_8) in_zero_width = false - rest.scan(WIDTH_SCANNER) do |gc| + rest.scan(WIDTH_SCANNER) do |non_printing_start, non_printing_end, csi, osc, gc| case - when gc[NON_PRINTING_START_INDEX] + when non_printing_start in_zero_width = true - when gc[NON_PRINTING_END_INDEX] + when non_printing_end in_zero_width = false - when gc[CSI_REGEXP_INDEX], gc[OSC_REGEXP_INDEX] - when gc[GRAPHEME_CLUSTER_INDEX] - gc = gc[GRAPHEME_CLUSTER_INDEX] + when csi, osc + when gc unless in_zero_width width += get_mbchar_width(gc) end @@ -154,30 +128,40 @@ class Reline::Unicode end end - def self.split_by_width(str, max_width, encoding = str.encoding) + def self.split_by_width(str, max_width, encoding = str.encoding, offset: 0) lines = [String.new(encoding: encoding)] height = 1 - width = 0 + width = offset rest = str.encode(Encoding::UTF_8) in_zero_width = false - rest.scan(WIDTH_SCANNER) do |gc| + seq = String.new(encoding: encoding) + rest.scan(WIDTH_SCANNER) do |non_printing_start, non_printing_end, csi, osc, gc| case - when gc[NON_PRINTING_START_INDEX] + when non_printing_start in_zero_width = true - when gc[NON_PRINTING_END_INDEX] + lines.last << NON_PRINTING_START + when non_printing_end in_zero_width = false - when gc[CSI_REGEXP_INDEX] - lines.last << gc[CSI_REGEXP_INDEX] - when gc[OSC_REGEXP_INDEX] - lines.last << gc[OSC_REGEXP_INDEX] - when gc[GRAPHEME_CLUSTER_INDEX] - gc = gc[GRAPHEME_CLUSTER_INDEX] + lines.last << NON_PRINTING_END + when csi + lines.last << csi + unless in_zero_width + if csi == -"\e[m" || csi == -"\e[0m" + seq.clear + else + seq << csi + end + end + when osc + lines.last << osc + seq << osc + when gc unless in_zero_width mbchar_width = get_mbchar_width(gc) if (width += mbchar_width) > max_width width = mbchar_width lines << nil - lines << String.new(encoding: encoding) + lines << seq.dup height += 1 end end @@ -194,23 +178,22 @@ class Reline::Unicode end # Take a chunk of a String cut by width with escape sequences. - def self.take_range(str, start_col, max_width, encoding = str.encoding) - chunk = String.new(encoding: encoding) + def self.take_range(str, start_col, max_width) + chunk = String.new(encoding: str.encoding) total_width = 0 rest = str.encode(Encoding::UTF_8) in_zero_width = false - rest.scan(WIDTH_SCANNER) do |gc| + rest.scan(WIDTH_SCANNER) do |non_printing_start, non_printing_end, csi, osc, gc| case - when gc[NON_PRINTING_START_INDEX] + when non_printing_start in_zero_width = true - when gc[NON_PRINTING_END_INDEX] + when non_printing_end in_zero_width = false - when gc[CSI_REGEXP_INDEX] - chunk << gc[CSI_REGEXP_INDEX] - when gc[OSC_REGEXP_INDEX] - chunk << gc[OSC_REGEXP_INDEX] - when gc[GRAPHEME_CLUSTER_INDEX] - gc = gc[GRAPHEME_CLUSTER_INDEX] + when csi + chunk << csi + when osc + chunk << osc + when gc if in_zero_width chunk << gc else diff --git a/lib/reline/unicode/east_asian_width.rb b/lib/reline/unicode/east_asian_width.rb index 89bc9d9435..fa16a1bb56 100644 --- a/lib/reline/unicode/east_asian_width.rb +++ b/lib/reline/unicode/east_asian_width.rb @@ -1,6 +1,6 @@ class Reline::Unicode::EastAsianWidth # This is based on EastAsianWidth.txt - # EastAsianWidth.txt + # UNICODE_VERSION = '15.1.0' # Fullwidth TYPE_F = /^[#{ %W( @@ -60,14 +60,14 @@ class Reline::Unicode::EastAsianWidth \u{2E80}-\u{2E99} \u{2E9B}-\u{2EF3} \u{2F00}-\u{2FD5} - \u{2FF0}-\u{2FFB} + \u{2FF0}-\u{2FFF} \u{3001}-\u{303E} \u{3041}-\u{3096} \u{3099}-\u{30FF} \u{3105}-\u{312F} \u{3131}-\u{318E} \u{3190}-\u{31E3} - \u{31F0}-\u{321E} + \u{31EF}-\u{321E} \u{3220}-\u{3247} \u{3250}-\u{4DBF} \u{4E00}-\u{A48C} @@ -84,8 +84,13 @@ class Reline::Unicode::EastAsianWidth \u{17000}-\u{187F7} \u{18800}-\u{18CD5} \u{18D00}-\u{18D08} - \u{1B000}-\u{1B11E} + \u{1AFF0}-\u{1AFF3} + \u{1AFF5}-\u{1AFFB} + \u{1AFFD}-\u{1AFFE} + \u{1B000}-\u{1B122} + \u{1B132} \u{1B150}-\u{1B152} + \u{1B155} \u{1B164}-\u{1B167} \u{1B170}-\u{1B2FB} \u{1F004} @@ -119,21 +124,21 @@ class Reline::Unicode::EastAsianWidth \u{1F6CC} \u{1F6D0}-\u{1F6D2} \u{1F6D5}-\u{1F6D7} + \u{1F6DC}-\u{1F6DF} \u{1F6EB}-\u{1F6EC} \u{1F6F4}-\u{1F6FC} \u{1F7E0}-\u{1F7EB} + \u{1F7F0} \u{1F90C}-\u{1F93A} \u{1F93C}-\u{1F945} - \u{1F947}-\u{1F978} - \u{1F97A}-\u{1F9CB} - \u{1F9CD}-\u{1F9FF} - \u{1FA70}-\u{1FA74} - \u{1FA78}-\u{1FA7A} - \u{1FA80}-\u{1FA86} - \u{1FA90}-\u{1FAA8} - \u{1FAB0}-\u{1FAB6} - \u{1FAC0}-\u{1FAC2} - \u{1FAD0}-\u{1FAD6} + \u{1F947}-\u{1F9FF} + \u{1FA70}-\u{1FA7C} + \u{1FA80}-\u{1FA88} + \u{1FA90}-\u{1FABD} + \u{1FABF}-\u{1FAC5} + \u{1FACE}-\u{1FADB} + \u{1FAE0}-\u{1FAE8} + \u{1FAF0}-\u{1FAF8} \u{20000}-\u{2FFFD} \u{30000}-\u{3FFFD} ).join }]/ @@ -403,8 +408,7 @@ class Reline::Unicode::EastAsianWidth \u{0591}-\u{05C7} \u{05D0}-\u{05EA} \u{05EF}-\u{05F4} - \u{0600}-\u{061C} - \u{061E}-\u{070D} + \u{0600}-\u{070D} \u{070F}-\u{074A} \u{074D}-\u{07B1} \u{07C0}-\u{07FA} @@ -413,9 +417,9 @@ class Reline::Unicode::EastAsianWidth \u{0840}-\u{085B} \u{085E} \u{0860}-\u{086A} - \u{08A0}-\u{08B4} - \u{08B6}-\u{08C7} - \u{08D3}-\u{0983} + \u{0870}-\u{088E} + \u{0890}-\u{0891} + \u{0898}-\u{0983} \u{0985}-\u{098C} \u{098F}-\u{0990} \u{0993}-\u{09A8} @@ -493,11 +497,12 @@ class Reline::Unicode::EastAsianWidth \u{0C0E}-\u{0C10} \u{0C12}-\u{0C28} \u{0C2A}-\u{0C39} - \u{0C3D}-\u{0C44} + \u{0C3C}-\u{0C44} \u{0C46}-\u{0C48} \u{0C4A}-\u{0C4D} \u{0C55}-\u{0C56} \u{0C58}-\u{0C5A} + \u{0C5D} \u{0C60}-\u{0C63} \u{0C66}-\u{0C6F} \u{0C77}-\u{0C8C} @@ -509,10 +514,10 @@ class Reline::Unicode::EastAsianWidth \u{0CC6}-\u{0CC8} \u{0CCA}-\u{0CCD} \u{0CD5}-\u{0CD6} - \u{0CDE} + \u{0CDD}-\u{0CDE} \u{0CE0}-\u{0CE3} \u{0CE6}-\u{0CEF} - \u{0CF1}-\u{0CF2} + \u{0CF1}-\u{0CF3} \u{0D00}-\u{0D0C} \u{0D0E}-\u{0D10} \u{0D12}-\u{0D44} @@ -542,7 +547,7 @@ class Reline::Unicode::EastAsianWidth \u{0EA7}-\u{0EBD} \u{0EC0}-\u{0EC4} \u{0EC6} - \u{0EC8}-\u{0ECD} + \u{0EC8}-\u{0ECE} \u{0ED0}-\u{0ED9} \u{0EDC}-\u{0EDF} \u{0F00}-\u{0F47} @@ -577,9 +582,8 @@ class Reline::Unicode::EastAsianWidth \u{13F8}-\u{13FD} \u{1400}-\u{169C} \u{16A0}-\u{16F8} - \u{1700}-\u{170C} - \u{170E}-\u{1714} - \u{1720}-\u{1736} + \u{1700}-\u{1715} + \u{171F}-\u{1736} \u{1740}-\u{1753} \u{1760}-\u{176C} \u{176E}-\u{1770} @@ -587,8 +591,7 @@ class Reline::Unicode::EastAsianWidth \u{1780}-\u{17DD} \u{17E0}-\u{17E9} \u{17F0}-\u{17F9} - \u{1800}-\u{180E} - \u{1810}-\u{1819} + \u{1800}-\u{1819} \u{1820}-\u{1878} \u{1880}-\u{18AA} \u{18B0}-\u{18F5} @@ -607,9 +610,9 @@ class Reline::Unicode::EastAsianWidth \u{1A7F}-\u{1A89} \u{1A90}-\u{1A99} \u{1AA0}-\u{1AAD} - \u{1AB0}-\u{1AC0} - \u{1B00}-\u{1B4B} - \u{1B50}-\u{1B7C} + \u{1AB0}-\u{1ACE} + \u{1B00}-\u{1B4C} + \u{1B50}-\u{1B7E} \u{1B80}-\u{1BF3} \u{1BFC}-\u{1C37} \u{1C3B}-\u{1C49} @@ -617,8 +620,7 @@ class Reline::Unicode::EastAsianWidth \u{1C90}-\u{1CBA} \u{1CBD}-\u{1CC7} \u{1CD0}-\u{1CFA} - \u{1D00}-\u{1DF9} - \u{1DFB}-\u{1F15} + \u{1D00}-\u{1F15} \u{1F18}-\u{1F1D} \u{1F20}-\u{1F45} \u{1F48}-\u{1F4D} @@ -653,7 +655,7 @@ class Reline::Unicode::EastAsianWidth \u{2090}-\u{209C} \u{20A0}-\u{20A8} \u{20AA}-\u{20AB} - \u{20AD}-\u{20BF} + \u{20AD}-\u{20C0} \u{20D0}-\u{20F0} \u{2100}-\u{2102} \u{2104} @@ -767,9 +769,7 @@ class Reline::Unicode::EastAsianWidth \u{2B51}-\u{2B54} \u{2B5A}-\u{2B73} \u{2B76}-\u{2B95} - \u{2B97}-\u{2C2E} - \u{2C30}-\u{2C5E} - \u{2C60}-\u{2CF3} + \u{2B97}-\u{2CF3} \u{2CF9}-\u{2D25} \u{2D27} \u{2D2D} @@ -784,14 +784,16 @@ class Reline::Unicode::EastAsianWidth \u{2DC8}-\u{2DCE} \u{2DD0}-\u{2DD6} \u{2DD8}-\u{2DDE} - \u{2DE0}-\u{2E52} + \u{2DE0}-\u{2E5D} \u{303F} \u{4DC0}-\u{4DFF} \u{A4D0}-\u{A62B} \u{A640}-\u{A6F7} - \u{A700}-\u{A7BF} - \u{A7C2}-\u{A7CA} - \u{A7F5}-\u{A82C} + \u{A700}-\u{A7CA} + \u{A7D0}-\u{A7D1} + \u{A7D3} + \u{A7D5}-\u{A7D9} + \u{A7F2}-\u{A82C} \u{A830}-\u{A839} \u{A840}-\u{A877} \u{A880}-\u{A8C5} @@ -823,11 +825,11 @@ class Reline::Unicode::EastAsianWidth \u{FB3E} \u{FB40}-\u{FB41} \u{FB43}-\u{FB44} - \u{FB46}-\u{FBC1} - \u{FBD3}-\u{FD3F} - \u{FD50}-\u{FD8F} + \u{FB46}-\u{FBC2} + \u{FBD3}-\u{FD8F} \u{FD92}-\u{FDC7} - \u{FDF0}-\u{FDFD} + \u{FDCF} + \u{FDF0}-\u{FDFF} \u{FE20}-\u{FE2F} \u{FE70}-\u{FE74} \u{FE76}-\u{FEFC} @@ -861,10 +863,20 @@ class Reline::Unicode::EastAsianWidth \u{104D8}-\u{104FB} \u{10500}-\u{10527} \u{10530}-\u{10563} - \u{1056F} + \u{1056F}-\u{1057A} + \u{1057C}-\u{1058A} + \u{1058C}-\u{10592} + \u{10594}-\u{10595} + \u{10597}-\u{105A1} + \u{105A3}-\u{105B1} + \u{105B3}-\u{105B9} + \u{105BB}-\u{105BC} \u{10600}-\u{10736} \u{10740}-\u{10755} \u{10760}-\u{10767} + \u{10780}-\u{10785} + \u{10787}-\u{107B0} + \u{107B2}-\u{107BA} \u{10800}-\u{10805} \u{10808} \u{1080A}-\u{10835} @@ -906,13 +918,14 @@ class Reline::Unicode::EastAsianWidth \u{10E80}-\u{10EA9} \u{10EAB}-\u{10EAD} \u{10EB0}-\u{10EB1} - \u{10F00}-\u{10F27} + \u{10EFD}-\u{10F27} \u{10F30}-\u{10F59} + \u{10F70}-\u{10F89} \u{10FB0}-\u{10FCB} \u{10FE0}-\u{10FF6} \u{11000}-\u{1104D} - \u{11052}-\u{1106F} - \u{1107F}-\u{110C1} + \u{11052}-\u{11075} + \u{1107F}-\u{110C2} \u{110CD} \u{110D0}-\u{110E8} \u{110F0}-\u{110F9} @@ -922,7 +935,7 @@ class Reline::Unicode::EastAsianWidth \u{11180}-\u{111DF} \u{111E1}-\u{111F4} \u{11200}-\u{11211} - \u{11213}-\u{1123E} + \u{11213}-\u{11241} \u{11280}-\u{11286} \u{11288} \u{1128A}-\u{1128D} @@ -954,11 +967,11 @@ class Reline::Unicode::EastAsianWidth \u{11600}-\u{11644} \u{11650}-\u{11659} \u{11660}-\u{1166C} - \u{11680}-\u{116B8} + \u{11680}-\u{116B9} \u{116C0}-\u{116C9} \u{11700}-\u{1171A} \u{1171D}-\u{1172B} - \u{11730}-\u{1173F} + \u{11730}-\u{11746} \u{11800}-\u{1183B} \u{118A0}-\u{118F2} \u{118FF}-\u{11906} @@ -974,7 +987,8 @@ class Reline::Unicode::EastAsianWidth \u{119DA}-\u{119E4} \u{11A00}-\u{11A47} \u{11A50}-\u{11AA2} - \u{11AC0}-\u{11AF8} + \u{11AB0}-\u{11AF8} + \u{11B00}-\u{11B09} \u{11C00}-\u{11C08} \u{11C0A}-\u{11C36} \u{11C38}-\u{11C45} @@ -996,19 +1010,23 @@ class Reline::Unicode::EastAsianWidth \u{11D93}-\u{11D98} \u{11DA0}-\u{11DA9} \u{11EE0}-\u{11EF8} + \u{11F00}-\u{11F10} + \u{11F12}-\u{11F3A} + \u{11F3E}-\u{11F59} \u{11FB0} \u{11FC0}-\u{11FF1} \u{11FFF}-\u{12399} \u{12400}-\u{1246E} \u{12470}-\u{12474} \u{12480}-\u{12543} - \u{13000}-\u{1342E} - \u{13430}-\u{13438} + \u{12F90}-\u{12FF2} + \u{13000}-\u{13455} \u{14400}-\u{14646} \u{16800}-\u{16A38} \u{16A40}-\u{16A5E} \u{16A60}-\u{16A69} - \u{16A6E}-\u{16A6F} + \u{16A6E}-\u{16ABE} + \u{16AC0}-\u{16AC9} \u{16AD0}-\u{16AED} \u{16AF0}-\u{16AF5} \u{16B00}-\u{16B45} @@ -1025,10 +1043,14 @@ class Reline::Unicode::EastAsianWidth \u{1BC80}-\u{1BC88} \u{1BC90}-\u{1BC99} \u{1BC9C}-\u{1BCA3} + \u{1CF00}-\u{1CF2D} + \u{1CF30}-\u{1CF46} + \u{1CF50}-\u{1CFC3} \u{1D000}-\u{1D0F5} \u{1D100}-\u{1D126} - \u{1D129}-\u{1D1E8} + \u{1D129}-\u{1D1EA} \u{1D200}-\u{1D245} + \u{1D2C0}-\u{1D2D3} \u{1D2E0}-\u{1D2F3} \u{1D300}-\u{1D356} \u{1D360}-\u{1D378} @@ -1055,17 +1077,27 @@ class Reline::Unicode::EastAsianWidth \u{1D7CE}-\u{1DA8B} \u{1DA9B}-\u{1DA9F} \u{1DAA1}-\u{1DAAF} + \u{1DF00}-\u{1DF1E} + \u{1DF25}-\u{1DF2A} \u{1E000}-\u{1E006} \u{1E008}-\u{1E018} \u{1E01B}-\u{1E021} \u{1E023}-\u{1E024} \u{1E026}-\u{1E02A} + \u{1E030}-\u{1E06D} + \u{1E08F} \u{1E100}-\u{1E12C} \u{1E130}-\u{1E13D} \u{1E140}-\u{1E149} \u{1E14E}-\u{1E14F} + \u{1E290}-\u{1E2AE} \u{1E2C0}-\u{1E2F9} \u{1E2FF} + \u{1E4D0}-\u{1E4F9} + \u{1E7E0}-\u{1E7E6} + \u{1E7E8}-\u{1E7EB} + \u{1E7ED}-\u{1E7EE} + \u{1E7F0}-\u{1E7FE} \u{1E800}-\u{1E8C4} \u{1E8C7}-\u{1E8D6} \u{1E900}-\u{1E94B} @@ -1142,8 +1174,8 @@ class Reline::Unicode::EastAsianWidth \u{1F6D3}-\u{1F6D4} \u{1F6E0}-\u{1F6EA} \u{1F6F0}-\u{1F6F3} - \u{1F700}-\u{1F773} - \u{1F780}-\u{1F7D8} + \u{1F700}-\u{1F776} + \u{1F77B}-\u{1F7D9} \u{1F800}-\u{1F80B} \u{1F810}-\u{1F847} \u{1F850}-\u{1F859} diff --git a/lib/reline/version.rb b/lib/reline/version.rb index 1bb1c02f3d..d68c7d203b 100644 --- a/lib/reline/version.rb +++ b/lib/reline/version.rb @@ -1,3 +1,3 @@ module Reline - VERSION = '0.3.1' + VERSION = '0.5.3' end diff --git a/lib/reline/windows.rb b/lib/reline/windows.rb index b952329911..ee3f73e383 100644 --- a/lib/reline/windows.rb +++ b/lib/reline/windows.rb @@ -1,6 +1,8 @@ require 'fiddle/import' class Reline::Windows + RESET_COLOR = "\e[0m" + def self.encoding Encoding::UTF_8 end @@ -85,7 +87,7 @@ class Reline::Windows def call(*args) import = @proto.split("") args.each_with_index do |x, i| - args[i], = [x == 0 ? nil : x].pack("p").unpack(POINTER_TYPE) if import[i] == "S" + args[i], = [x == 0 ? nil : +x].pack("p").unpack(POINTER_TYPE) if import[i] == "S" args[i], = [x].pack("I").unpack("i") if import[i] == "I" end ret, = @func.call(*args) @@ -257,7 +259,7 @@ class Reline::Windows def self.check_input_event num_of_events = 0.chr * 8 while @@output_buf.empty? - Reline.core.line_editor.resize + Reline.core.line_editor.handle_signal if @@WaitForSingleObject.(@@hConsoleInputHandle, 100) != 0 # max 0.1 sec # prevent for background consolemode change @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) @@ -291,7 +293,11 @@ class Reline::Windows end end - def self.getc + def self.with_raw_input + yield + end + + def self.getc(_timeout_second) check_input_event @@output_buf.shift end |