summaryrefslogtreecommitdiff
path: root/lib/reline
diff options
context:
space:
mode:
Diffstat (limited to 'lib/reline')
-rw-r--r--lib/reline/ansi.rb357
-rw-r--r--lib/reline/config.rb93
-rw-r--r--lib/reline/face.rb199
-rw-r--r--lib/reline/general_io.rb113
-rw-r--r--lib/reline/history.rb2
-rw-r--r--lib/reline/io.rb45
-rw-r--r--lib/reline/io/ansi.rb358
-rw-r--r--lib/reline/io/dumb.rb106
-rw-r--r--lib/reline/io/windows.rb (renamed from lib/reline/windows.rb)208
-rw-r--r--lib/reline/key_actor/base.rb4
-rw-r--r--lib/reline/key_actor/emacs.rb26
-rw-r--r--lib/reline/key_actor/vi_command.rb46
-rw-r--r--lib/reline/key_actor/vi_insert.rb10
-rw-r--r--lib/reline/key_stroke.rb57
-rw-r--r--lib/reline/kill_ring.rb4
-rw-r--r--lib/reline/line_editor.rb2839
-rw-r--r--lib/reline/reline.gemspec5
-rw-r--r--lib/reline/terminfo.rb21
-rw-r--r--lib/reline/unicode.rb98
-rw-r--r--lib/reline/unicode/east_asian_width.rb6
-rw-r--r--lib/reline/version.rb2
21 files changed, 2077 insertions, 2522 deletions
diff --git a/lib/reline/ansi.rb b/lib/reline/ansi.rb
deleted file mode 100644
index 06ea9efd09..0000000000
--- a/lib/reline/ansi.rb
+++ /dev/null
@@ -1,357 +0,0 @@
-require 'io/console'
-require 'io/wait'
-require 'timeout'
-require_relative 'terminfo'
-
-class Reline::ANSI
- 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,
- }
-
- if Reline::Terminfo.enabled?
- Reline::Terminfo.setupterm(0, 2)
- end
-
- def self.encoding
- Encoding.default_external
- end
-
- def self.win?
- false
- end
-
- def self.set_default_key_bindings(config, allow_terminfo: true)
- 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)
- config.add_default_key_binding_by_keymap(:vi_insert, key, func)
- end
- {
- # default bindings
- [27, 32] => :em_set_mark, # M-<space>
- [24, 24] => :em_exchange_mark, # C-x C-x
- }.each_pair do |key, func|
- config.add_default_key_binding_by_keymap(:emacs, key, func)
- 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
- rescue Reline::Terminfo::TerminfoError
- # capname is undefined
- end
- end.compact.to_h
-
- key_bindings.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
- end
-
- def self.set_default_key_bindings_comprehensive_list(config)
- {
- # Console (80x25)
- [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, # ↓
- [27, 71, 67] => :ed_next_char, # →
- [27, 71, 68] => :ed_prev_char, # ←
-
- # urxvt / exoterm
- [27, 91, 55, 126] => :ed_move_to_beg, # Home
- [27, 91, 56, 126] => :ed_move_to_end, # End
-
- # GNOME
- [27, 79, 72] => :ed_move_to_beg, # Home
- [27, 79, 70] => :ed_move_to_end, # End
- # 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, # →
- [27, 79, 68] => :ed_prev_char, # ←
- }.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
- end
-
- @@input = STDIN
- def self.input=(val)
- @@input = val
- end
-
- @@output = STDOUT
- def self.output=(val)
- @@output = val
- end
-
- def self.with_raw_input
- @@input.raw { yield }
- end
-
- @@buf = []
- def self.inner_getc
- 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
- end
- (c == 0x16 && @@input.raw(min: 0, time: 0, &:getbyte)) || c
- rescue Errno::EIO
- # Maybe the I/O has been closed.
- nil
- rescue Errno::ENOTTY
- nil
- end
-
- @@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
- buffer = String.new(encoding: Encoding::ASCII_8BIT)
- buffer << inner_getc
- 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
- elsif END_BRACKETED_PASTE == buffer
- @@in_bracketed_paste_mode = false
- ungetc(-1)
- return inner_getc
- end
- begin
- succ_c = nil
- Timeout.timeout(Reline.core.config.keyseq_timeout * 100) {
- succ_c = inner_getc
- }
- rescue Timeout::Error
- break
- else
- buffer << succ_c
- end
- end
- buffer.bytes.reverse_each do |ch|
- ungetc ch
- end
- inner_getc
- end
-
- def self.getc
- if Reline.core.config.enable_bracketed_paste
- getc_with_bracketed_paste
- else
- inner_getc
- end
- end
-
- def self.in_pasting?
- @@in_bracketed_paste_mode or (not Reline::IOGate.empty_buffer?)
- end
-
- def self.empty_buffer?
- unless @@buf.empty?
- return false
- end
- !@@input.wait_readable(0)
- end
-
- def self.ungetc(c)
- @@buf.unshift(c)
- end
-
- def self.retrieve_keybuffer
- begin
- return unless @@input.wait_readable(0.001)
- str = @@input.read_nonblock(1024)
- str.bytes.each do |c|
- @@buf.push(c)
- end
- rescue EOFError
- end
- end
-
- def self.get_screen_size
- s = @@input.winsize
- return s if s[0] > 0 && s[1] > 0
- s = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i]
- return s if s[0] > 0 && s[1] > 0
- [24, 80]
- rescue Errno::ENOTTY
- [24, 80]
- end
-
- def self.set_screen_size(rows, columns)
- @@input.winsize = [rows, columns]
- self
- rescue Errno::ENOTTY
- self
- end
-
- def self.cursor_pos
- begin
- res = +''
- m = nil
- @@input.raw do |stdin|
- @@output << "\e[6n"
- @@output.flush
- loop do
- c = stdin.getc
- next if c.nil?
- res << c
- m = res.match(/\e\[(?<row>\d+);(?<column>\d+)R/)
- break if m
- end
- (m.pre_match + m.post_match).chars.reverse_each do |ch|
- stdin.ungetc ch
- end
- end
- column = m[:column].to_i - 1
- row = m[:row].to_i - 1
- rescue Errno::ENOTTY
- begin
- buf = @@output.pread(@@output.pos, 0)
- row = buf.count("\n")
- column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0
- rescue Errno::ESPIPE
- # Just returns column 1 for ambiguous width because this I/O is not
- # tty and can't seek.
- row = 0
- column = 1
- end
- end
- Reline::CursorPos.new(column, row)
- end
-
- def self.move_cursor_column(x)
- @@output.write "\e[#{x + 1}G"
- end
-
- def self.move_cursor_up(x)
- if x > 0
- @@output.write "\e[#{x}A"
- elsif x < 0
- move_cursor_down(-x)
- end
- end
-
- def self.move_cursor_down(x)
- if x > 0
- @@output.write "\e[#{x}B"
- elsif x < 0
- move_cursor_up(-x)
- end
- end
-
- def self.hide_cursor
- if Reline::Terminfo.enabled?
- begin
- @@output.write Reline::Terminfo.tigetstr('civis')
- rescue Reline::Terminfo::TerminfoError
- # civis is undefined
- end
- else
- # ignored
- end
- end
-
- def self.show_cursor
- if Reline::Terminfo.enabled?
- begin
- @@output.write Reline::Terminfo.tigetstr('cnorm')
- rescue Reline::Terminfo::TerminfoError
- # cnorm is undefined
- end
- else
- # ignored
- end
- end
-
- def self.erase_after_cursor
- @@output.write "\e[K"
- end
-
- def self.scroll_down(x)
- return if x.zero?
- @@output.write "\e[#{x}S"
- end
-
- def self.clear_screen
- @@output.write "\e[2J"
- @@output.write "\e[1;1H"
- end
-
- @@old_winch_handler = nil
- def self.set_winch_handler(&handler)
- @@old_winch_handler = Signal.trap('WINCH', &handler)
- end
-
- def self.prep
- retrieve_keybuffer
- nil
- end
-
- def self.deprep(otio)
- Signal.trap('WINCH', @@old_winch_handler) if @@old_winch_handler
- end
-end
diff --git a/lib/reline/config.rb b/lib/reline/config.rb
index 87726393a6..62c6d105b3 100644
--- a/lib/reline/config.rb
+++ b/lib/reline/config.rb
@@ -8,31 +8,12 @@ class Reline::Config
end
VARIABLE_NAMES = %w{
- bind-tty-special-chars
- blink-matching-paren
- byte-oriented
completion-ignore-case
convert-meta
disable-completion
- enable-keypad
- expand-tilde
- history-preserve-point
history-size
- horizontal-scroll-mode
- input-meta
keyseq-timeout
- mark-directories
- mark-modified-lines
- mark-symlinked-directories
- match-hidden-files
- meta-flag
- output-meta
- page-completions
- prefer-visible-bell
- print-completions-horizontally
show-all-if-ambiguous
- show-all-if-unmodified
- visible-stats
show-mode-in-prompt
vi-cmd-mode-string
vi-ins-mode-string
@@ -53,8 +34,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 = []
@@ -71,17 +50,15 @@ class Reline::Config
@test_mode = false
@autocompletion = false
@convert_meta = true if seven_bit_encoding?(Reline::IOGate.encoding)
+ @loaded = false
+ @enable_bracketed_paste = true
end
def reset
if editing_mode_is?(:vi_command)
@editing_mode_label = :vi_insert
end
- @additional_key_bindings.keys.each do |key|
- @additional_key_bindings[key].clear
- end
@oneshot_key_bindings.clear
- reset_default_key_bindings
end
def editing_mode
@@ -100,6 +77,10 @@ class Reline::Config
@key_actors[@keymap_label]
end
+ def loaded?
+ @loaded
+ end
+
def inputrc_path
case ENV['INPUTRC']
when nil, ''
@@ -131,6 +112,7 @@ class Reline::Config
end
def read(file = nil)
+ @loaded = true
file ||= default_inputrc_path
begin
if file.respond_to?(:readlines)
@@ -173,12 +155,6 @@ class Reline::Config
@key_actors[@keymap_label].default_key_bindings[keystroke] = target
end
- def reset_default_key_bindings
- @key_actors.values.each do |ka|
- ka.reset_default_key_bindings
- end
- end
-
def read_lines(lines, file = nil)
if not lines.empty? and lines.first.encoding != Reline.encoding_system_needs
begin
@@ -190,9 +166,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*#/)
@@ -201,62 +175,67 @@ 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
- var, value = $1.downcase, $2
- bind_variable(var, value)
+ when /^set +([^ ]+) +(.+)/i
+ # value ignores everything after a space, raw_value does not.
+ var, value, raw_value = $1.downcase, $2.partition(' ').first, $2
+ bind_variable(var, value, raw_value)
next
when /\s*("#{KEYSEQ_PATTERN}+")\s*:\s*(.*)\s*$/o
key, func_name = $1, $2
+ func_name = func_name.split.first
keystroke, func = bind_key(key, func_name)
next unless keystroke
@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'
condition = false
case args
- when 'mode'
+ when /^mode=(vi|emacs)$/i
+ mode = $1.downcase
+ # NOTE: mode=vi means vi-insert mode
+ mode = 'vi_insert' if mode == 'vi'
+ if @editing_mode_label == mode.to_sym
+ condition = true
+ end
when 'term'
when 'version'
else # application name
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
- def bind_variable(name, value)
+ def bind_variable(name, value, raw_value)
case name
when 'history-size'
begin
@@ -281,7 +260,7 @@ class Reline::Config
when 'completion-query-items'
@completion_query_items = value.to_i
when 'isearch-terminators'
- @isearch_terminators = retrieve_string(value)
+ @isearch_terminators = retrieve_string(raw_value)
when 'editing-mode'
case value
when 'emacs'
@@ -323,11 +302,11 @@ class Reline::Config
@show_mode_in_prompt = false
end
when 'vi-cmd-mode-string'
- @vi_cmd_mode_string = retrieve_string(value)
+ @vi_cmd_mode_string = retrieve_string(raw_value)
when 'vi-ins-mode-string'
- @vi_ins_mode_string = retrieve_string(value)
+ @vi_ins_mode_string = retrieve_string(raw_value)
when 'emacs-mode-string'
- @emacs_mode_string = retrieve_string(value)
+ @emacs_mode_string = retrieve_string(raw_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
deleted file mode 100644
index 9929846568..0000000000
--- a/lib/reline/general_io.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-require 'timeout'
-require 'io/wait'
-
-class Reline::GeneralIO
- def self.reset(encoding: nil)
- @@pasting = false
- @@encoding = encoding
- end
-
- def self.encoding
- if defined?(@@encoding)
- @@encoding
- elsif RUBY_PLATFORM =~ /mswin|mingw/
- Encoding::UTF_8
- else
- Encoding::default_external
- end
- end
-
- def self.win?
- false
- end
-
- def self.set_default_key_bindings(_)
- end
-
- @@buf = []
- @@input = STDIN
-
- def self.input=(val)
- @@input = val
- end
-
- def self.with_raw_input
- yield
- end
-
- def self.getc
- unless @@buf.empty?
- return @@buf.shift
- end
- c = nil
- loop do
- result = @@input.wait_readable(0.1)
- next if result.nil?
- c = @@input.read(1)
- break
- end
- c&.ord
- end
-
- def self.ungetc(c)
- @@buf.unshift(c)
- end
-
- def self.get_screen_size
- [1, 1]
- 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
-
- def self.move_cursor_up(val)
- end
-
- def self.move_cursor_down(val)
- end
-
- def self.erase_after_cursor
- end
-
- def self.scroll_down(val)
- end
-
- def self.clear_screen
- end
-
- def self.set_screen_size(rows, columns)
- end
-
- def self.set_winch_handler(&handler)
- end
-
- @@pasting = false
-
- def self.in_pasting?
- @@pasting
- end
-
- def self.start_pasting
- @@pasting = true
- end
-
- def self.finish_pasting
- @@pasting = false
- end
-
- def self.prep
- end
-
- def self.deprep(otio)
- end
-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/io.rb b/lib/reline/io.rb
new file mode 100644
index 0000000000..7fca0c338a
--- /dev/null
+++ b/lib/reline/io.rb
@@ -0,0 +1,45 @@
+
+module Reline
+ class IO
+ RESET_COLOR = "\e[0m"
+
+ def self.decide_io_gate
+ if ENV['TERM'] == 'dumb'
+ Reline::Dumb.new
+ else
+ require 'reline/io/ansi'
+
+ case RbConfig::CONFIG['host_os']
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
+ require 'reline/io/windows'
+ io = Reline::Windows.new
+ if io.msys_tty?
+ Reline::ANSI.new
+ else
+ io
+ end
+ else
+ if $stdout.tty?
+ Reline::ANSI.new
+ else
+ Reline::Dumb.new
+ end
+ end
+ end
+ end
+
+ def dumb?
+ false
+ end
+
+ def win?
+ false
+ end
+
+ def reset_color_sequence
+ self.class::RESET_COLOR
+ end
+ end
+end
+
+require 'reline/io/dumb'
diff --git a/lib/reline/io/ansi.rb b/lib/reline/io/ansi.rb
new file mode 100644
index 0000000000..cf3c9965dd
--- /dev/null
+++ b/lib/reline/io/ansi.rb
@@ -0,0 +1,358 @@
+require 'io/console'
+require 'io/wait'
+
+class Reline::ANSI < Reline::IO
+ 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,
+ }
+
+ 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?
+ Reline::Terminfo.setupterm(0, 2)
+ end
+
+ def initialize
+ @input = STDIN
+ @output = STDOUT
+ @buf = []
+ @old_winch_handler = nil
+ end
+
+ def encoding
+ Encoding.default_external
+ end
+
+ def set_default_key_bindings(config, allow_terminfo: true)
+ set_bracketed_paste_key_bindings(config)
+ 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
+ {
+ [27, 91, 90] => :completion_journey_up, # S-Tab
+ }.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)
+ end
+ {
+ # default bindings
+ [27, 32] => :em_set_mark, # M-<space>
+ [24, 24] => :em_exchange_mark, # C-x C-x
+ }.each_pair do |key, func|
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
+ end
+ end
+
+ def set_bracketed_paste_key_bindings(config)
+ [:emacs, :vi_insert, :vi_command].each do |keymap|
+ config.add_default_key_binding_by_keymap(keymap, START_BRACKETED_PASTE.bytes, :bracketed_paste_start)
+ end
+ end
+
+ def 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 set_default_key_bindings_terminfo(config)
+ key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding|
+ begin
+ key_code = Reline::Terminfo.tigetstr(capname)
+ [ key_code.bytes, key_binding ]
+ rescue Reline::Terminfo::TerminfoError
+ # capname is undefined
+ end
+ end.compact.to_h
+
+ key_bindings.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
+ end
+
+ def set_default_key_bindings_comprehensive_list(config)
+ {
+ # Console (80x25)
+ [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
+
+ # KDE
+ # Del is 0x08
+ [27, 71, 65] => :ed_prev_history, # ↑
+ [27, 71, 66] => :ed_next_history, # ↓
+ [27, 71, 67] => :ed_next_char, # →
+ [27, 71, 68] => :ed_prev_char, # ←
+
+ # urxvt / exoterm
+ [27, 91, 55, 126] => :ed_move_to_beg, # Home
+ [27, 91, 56, 126] => :ed_move_to_end, # End
+
+ # GNOME
+ [27, 79, 72] => :ed_move_to_beg, # Home
+ [27, 79, 70] => :ed_move_to_end, # End
+ # Del is 0x08
+ # Arrow keys are the same of KDE
+
+ [27, 79, 65] => :ed_prev_history, # ↑
+ [27, 79, 66] => :ed_next_history, # ↓
+ [27, 79, 67] => :ed_next_char, # →
+ [27, 79, 68] => :ed_prev_char, # ←
+ }.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
+ end
+
+ def input=(val)
+ @input = val
+ end
+
+ def output=(val)
+ @output = val
+ end
+
+ def with_raw_input
+ if @input.tty?
+ @input.raw(intr: true) { yield }
+ else
+ yield
+ end
+ end
+
+ def inner_getc(timeout_second)
+ unless @buf.empty?
+ return @buf.shift
+ end
+ until @input.wait_readable(0.01)
+ timeout_second -= 0.01
+ return nil if timeout_second <= 0
+
+ Reline.core.line_editor.handle_signal
+ end
+ c = @input.getbyte
+ (c == 0x16 && @input.raw(min: 0, time: 0, &:getbyte)) || c
+ rescue Errno::EIO
+ # Maybe the I/O has been closed.
+ nil
+ rescue Errno::ENOTTY
+ nil
+ end
+
+ START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT)
+ END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT)
+ def read_bracketed_paste
+ buffer = String.new(encoding: Encoding::ASCII_8BIT)
+ until buffer.end_with?(END_BRACKETED_PASTE)
+ c = inner_getc(Float::INFINITY)
+ break unless c
+ buffer << c
+ end
+ string = buffer.delete_suffix(END_BRACKETED_PASTE).force_encoding(encoding)
+ string.valid_encoding? ? string : ''
+ end
+
+ # if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second
+ def getc(timeout_second)
+ inner_getc(timeout_second)
+ end
+
+ def in_pasting?
+ not empty_buffer?
+ end
+
+ def empty_buffer?
+ unless @buf.empty?
+ return false
+ end
+ !@input.wait_readable(0)
+ end
+
+ def ungetc(c)
+ @buf.unshift(c)
+ end
+
+ def retrieve_keybuffer
+ begin
+ return unless @input.wait_readable(0.001)
+ str = @input.read_nonblock(1024)
+ str.bytes.each do |c|
+ @buf.push(c)
+ end
+ rescue EOFError
+ end
+ end
+
+ def get_screen_size
+ s = @input.winsize
+ return s if s[0] > 0 && s[1] > 0
+ s = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i]
+ return s if s[0] > 0 && s[1] > 0
+ [24, 80]
+ rescue Errno::ENOTTY, Errno::ENODEV
+ [24, 80]
+ end
+
+ def set_screen_size(rows, columns)
+ @input.winsize = [rows, columns]
+ self
+ rescue Errno::ENOTTY
+ self
+ end
+
+ def cursor_pos
+ begin
+ res = +''
+ m = nil
+ @input.raw do |stdin|
+ @output << "\e[6n"
+ @output.flush
+ loop do
+ c = stdin.getc
+ next if c.nil?
+ res << c
+ m = res.match(/\e\[(?<row>\d+);(?<column>\d+)R/)
+ break if m
+ end
+ (m.pre_match + m.post_match).chars.reverse_each do |ch|
+ stdin.ungetc ch
+ end
+ end
+ column = m[:column].to_i - 1
+ row = m[:row].to_i - 1
+ rescue Errno::ENOTTY
+ begin
+ buf = @output.pread(@output.pos, 0)
+ row = buf.count("\n")
+ column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0
+ rescue Errno::ESPIPE, IOError
+ # Just returns column 1 for ambiguous width because this I/O is not
+ # tty and can't seek.
+ row = 0
+ column = 1
+ end
+ end
+ Reline::CursorPos.new(column, row)
+ end
+
+ def move_cursor_column(x)
+ @output.write "\e[#{x + 1}G"
+ end
+
+ def move_cursor_up(x)
+ if x > 0
+ @output.write "\e[#{x}A"
+ elsif x < 0
+ move_cursor_down(-x)
+ end
+ end
+
+ def move_cursor_down(x)
+ if x > 0
+ @output.write "\e[#{x}B"
+ elsif x < 0
+ move_cursor_up(-x)
+ end
+ end
+
+ def hide_cursor
+ if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
+ begin
+ @output.write Reline::Terminfo.tigetstr('civis')
+ rescue Reline::Terminfo::TerminfoError
+ # civis is undefined
+ end
+ else
+ # ignored
+ end
+ end
+
+ def show_cursor
+ if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
+ begin
+ @output.write Reline::Terminfo.tigetstr('cnorm')
+ rescue Reline::Terminfo::TerminfoError
+ # cnorm is undefined
+ end
+ else
+ # ignored
+ end
+ end
+
+ def erase_after_cursor
+ @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 scroll_down(x)
+ return if x.zero?
+ # 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 clear_screen
+ @output.write "\e[2J"
+ @output.write "\e[1;1H"
+ end
+
+ def set_winch_handler(&handler)
+ @old_winch_handler = Signal.trap('WINCH', &handler)
+ end
+
+ def prep
+ # Enable bracketed paste
+ @output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste
+ retrieve_keybuffer
+ nil
+ end
+
+ def deprep(otio)
+ # Disable bracketed paste
+ @output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste
+ Signal.trap('WINCH', @old_winch_handler) if @old_winch_handler
+ end
+end
diff --git a/lib/reline/io/dumb.rb b/lib/reline/io/dumb.rb
new file mode 100644
index 0000000000..6ed69ffdfa
--- /dev/null
+++ b/lib/reline/io/dumb.rb
@@ -0,0 +1,106 @@
+require 'io/wait'
+
+class Reline::Dumb < Reline::IO
+ RESET_COLOR = '' # Do not send color reset sequence
+
+ def initialize(encoding: nil)
+ @input = STDIN
+ @buf = []
+ @pasting = false
+ @encoding = encoding
+ @screen_size = [24, 80]
+ end
+
+ def dumb?
+ true
+ end
+
+ def encoding
+ if @encoding
+ @encoding
+ elsif RUBY_PLATFORM =~ /mswin|mingw/
+ Encoding::UTF_8
+ else
+ Encoding::default_external
+ end
+ end
+
+ def set_default_key_bindings(_)
+ end
+
+ def input=(val)
+ @input = val
+ end
+
+ def with_raw_input
+ yield
+ end
+
+ def 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)
+ break
+ end
+ c&.ord
+ end
+
+ def ungetc(c)
+ @buf.unshift(c)
+ end
+
+ def get_screen_size
+ @screen_size
+ end
+
+ def cursor_pos
+ Reline::CursorPos.new(1, 1)
+ end
+
+ def hide_cursor
+ end
+
+ def show_cursor
+ end
+
+ def move_cursor_column(val)
+ end
+
+ def move_cursor_up(val)
+ end
+
+ def move_cursor_down(val)
+ end
+
+ def erase_after_cursor
+ end
+
+ def scroll_down(val)
+ end
+
+ def clear_screen
+ end
+
+ def set_screen_size(rows, columns)
+ @screen_size = [rows, columns]
+ end
+
+ def set_winch_handler(&handler)
+ end
+
+ def in_pasting?
+ @pasting
+ end
+
+ def prep
+ end
+
+ def deprep(otio)
+ end
+end
diff --git a/lib/reline/windows.rb b/lib/reline/io/windows.rb
index 7ea2a00f63..6ba4b830d6 100644
--- a/lib/reline/windows.rb
+++ b/lib/reline/io/windows.rb
@@ -1,19 +1,49 @@
require 'fiddle/import'
-class Reline::Windows
- def self.encoding
+class Reline::Windows < Reline::IO
+ def initialize
+ @input_buf = []
+ @output_buf = []
+
+ @output = STDOUT
+ @hsg = nil
+ @getwch = Win32API.new('msvcrt', '_getwch', [], 'I')
+ @kbhit = Win32API.new('msvcrt', '_kbhit', [], 'I')
+ @GetKeyState = Win32API.new('user32', 'GetKeyState', ['L'], 'L')
+ @GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L')
+ @SetConsoleCursorPosition = Win32API.new('kernel32', 'SetConsoleCursorPosition', ['L', 'L'], 'L')
+ @GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L')
+ @FillConsoleOutputCharacter = Win32API.new('kernel32', 'FillConsoleOutputCharacter', ['L', 'L', 'L', 'L', 'P'], 'L')
+ @ScrollConsoleScreenBuffer = Win32API.new('kernel32', 'ScrollConsoleScreenBuffer', ['L', 'P', 'P', 'L', 'P'], 'L')
+ @hConsoleHandle = @GetStdHandle.call(STD_OUTPUT_HANDLE)
+ @hConsoleInputHandle = @GetStdHandle.call(STD_INPUT_HANDLE)
+ @GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L')
+ @ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
+ @GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
+ @GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
+ @FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
+ @SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L')
+
+ @GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
+ @SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
+ @WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L')
+
+ @legacy_console = getconsolemode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0
+ end
+
+ def encoding
Encoding::UTF_8
end
- def self.win?
+ def win?
true
end
- def self.win_legacy_console?
- @@legacy_console
+ def win_legacy_console?
+ @legacy_console
end
- def self.set_default_key_bindings(config)
+ def set_default_key_bindings(config)
{
[224, 72] => :ed_prev_history, # ↑
[224, 80] => :ed_next_history, # ↓
@@ -85,7 +115,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)
@@ -127,58 +157,32 @@ class Reline::Windows
STD_OUTPUT_HANDLE = -11
FILE_TYPE_PIPE = 0x0003
FILE_NAME_INFO = 2
- @@getwch = Win32API.new('msvcrt', '_getwch', [], 'I')
- @@kbhit = Win32API.new('msvcrt', '_kbhit', [], 'I')
- @@GetKeyState = Win32API.new('user32', 'GetKeyState', ['L'], 'L')
- @@GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L')
- @@SetConsoleCursorPosition = Win32API.new('kernel32', 'SetConsoleCursorPosition', ['L', 'L'], 'L')
- @@GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L')
- @@FillConsoleOutputCharacter = Win32API.new('kernel32', 'FillConsoleOutputCharacter', ['L', 'L', 'L', 'L', 'P'], 'L')
- @@ScrollConsoleScreenBuffer = Win32API.new('kernel32', 'ScrollConsoleScreenBuffer', ['L', 'P', 'P', 'L', 'P'], 'L')
- @@hConsoleHandle = @@GetStdHandle.call(STD_OUTPUT_HANDLE)
- @@hConsoleInputHandle = @@GetStdHandle.call(STD_INPUT_HANDLE)
- @@GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L')
- @@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
- @@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
- @@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
- @@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
- @@SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L')
-
- @@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
- @@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
- @@WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L')
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
- private_class_method def self.getconsolemode
+ private def getconsolemode
mode = "\000\000\000\000"
- @@GetConsoleMode.call(@@hConsoleHandle, mode)
+ @GetConsoleMode.call(@hConsoleHandle, mode)
mode.unpack1('L')
end
- private_class_method def self.setconsolemode(mode)
- @@SetConsoleMode.call(@@hConsoleHandle, mode)
+ private def setconsolemode(mode)
+ @SetConsoleMode.call(@hConsoleHandle, mode)
end
- @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
- #if @@legacy_console
+ #if @legacy_console
# setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
- # @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
+ # @legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
#end
- @@input_buf = []
- @@output_buf = []
-
- @@output = STDOUT
-
- def self.msys_tty?(io = @@hConsoleInputHandle)
+ def msys_tty?(io = @hConsoleInputHandle)
# check if fd is a pipe
- if @@GetFileType.call(io) != FILE_TYPE_PIPE
+ if @GetFileType.call(io) != FILE_TYPE_PIPE
return false
end
bufsize = 1024
p_buffer = "\0" * bufsize
- res = @@GetFileInformationByHandleEx.call(io, FILE_NAME_INFO, p_buffer, bufsize - 2)
+ res = @GetFileInformationByHandleEx.call(io, FILE_NAME_INFO, p_buffer, bufsize - 2)
return false if res == 0
# get pipe name: p_buffer layout is:
@@ -215,65 +219,63 @@ class Reline::Windows
[ { control_keys: :SHIFT, virtual_key_code: VK_TAB }, [27, 91, 90] ],
]
- @@hsg = nil
-
- def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
+ def process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
# high-surrogate
if 0xD800 <= char_code and char_code <= 0xDBFF
- @@hsg = char_code
+ @hsg = char_code
return
end
# low-surrogate
if 0xDC00 <= char_code and char_code <= 0xDFFF
- if @@hsg
- char_code = 0x10000 + (@@hsg - 0xD800) * 0x400 + char_code - 0xDC00
- @@hsg = nil
+ if @hsg
+ char_code = 0x10000 + (@hsg - 0xD800) * 0x400 + char_code - 0xDC00
+ @hsg = nil
else
# no high-surrogate. ignored.
return
end
else
# ignore high-surrogate without low-surrogate if there
- @@hsg = nil
+ @hsg = nil
end
key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state)
match = KEY_MAP.find { |args,| key.matches?(**args) }
unless match.nil?
- @@output_buf.concat(match.last)
+ @output_buf.concat(match.last)
return
end
# no char, only control keys
return if key.char_code == 0 and key.control_keys.any?
- @@output_buf.push("\e".ord) if key.control_keys.include?(:ALT) and !key.control_keys.include?(:CTRL)
+ @output_buf.push("\e".ord) if key.control_keys.include?(:ALT) and !key.control_keys.include?(:CTRL)
- @@output_buf.concat(key.char.bytes)
+ @output_buf.concat(key.char.bytes)
end
- def self.check_input_event
+ def check_input_event
num_of_events = 0.chr * 8
- while @@output_buf.empty?
- Reline.core.line_editor.resize
- if @@WaitForSingleObject.(@@hConsoleInputHandle, 100) != 0 # max 0.1 sec
+ while @output_buf.empty?
+ 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)
+ @legacy_console = getconsolemode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0
next
end
- next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack1('L') == 0
+ next if @GetNumberOfConsoleInputEvents.(@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack1('L') == 0
input_records = 0.chr * 20 * 80
read_event = 0.chr * 4
- if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_records, 80, read_event) != 0
+ if @ReadConsoleInputW.(@hConsoleInputHandle, input_records, 80, read_event) != 0
read_events = read_event.unpack1('L')
0.upto(read_events) do |idx|
input_record = input_records[idx * 20, 20]
event = input_record[0, 2].unpack1('s*')
case event
when WINDOW_BUFFER_SIZE_EVENT
- @@winch_handler.()
+ @winch_handler.()
when KEY_EVENT
key_down = input_record[4, 4].unpack1('l*')
repeat_count = input_record[8, 2].unpack1('s*')
@@ -291,34 +293,34 @@ class Reline::Windows
end
end
- def self.with_raw_input
+ def with_raw_input
yield
end
- def self.getc
+ def getc(_timeout_second)
check_input_event
- @@output_buf.shift
+ @output_buf.shift
end
- def self.ungetc(c)
- @@output_buf.unshift(c)
+ def ungetc(c)
+ @output_buf.unshift(c)
end
- def self.in_pasting?
- not self.empty_buffer?
+ def in_pasting?
+ not empty_buffer?
end
- def self.empty_buffer?
- if not @@output_buf.empty?
+ def empty_buffer?
+ if not @output_buf.empty?
false
- elsif @@kbhit.call == 0
+ elsif @kbhit.call == 0
true
else
false
end
end
- def self.get_console_screen_buffer_info
+ def get_console_screen_buffer_info
# CONSOLE_SCREEN_BUFFER_INFO
# [ 0,2] dwSize.X
# [ 2,2] dwSize.Y
@@ -332,18 +334,18 @@ class Reline::Windows
# [18,2] dwMaximumWindowSize.X
# [20,2] dwMaximumWindowSize.Y
csbi = 0.chr * 22
- return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0
+ return if @GetConsoleScreenBufferInfo.call(@hConsoleHandle, csbi) == 0
csbi
end
- def self.get_screen_size
+ def get_screen_size
unless csbi = get_console_screen_buffer_info
return [1, 1]
end
csbi[0, 4].unpack('SS').reverse
end
- def self.cursor_pos
+ def cursor_pos
unless csbi = get_console_screen_buffer_info
return Reline::CursorPos.new(0, 0)
end
@@ -352,49 +354,49 @@ class Reline::Windows
Reline::CursorPos.new(x, y)
end
- def self.move_cursor_column(val)
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, cursor_pos.y * 65536 + val)
+ def move_cursor_column(val)
+ @SetConsoleCursorPosition.call(@hConsoleHandle, cursor_pos.y * 65536 + val)
end
- def self.move_cursor_up(val)
+ def move_cursor_up(val)
if val > 0
y = cursor_pos.y - val
y = 0 if y < 0
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, y * 65536 + cursor_pos.x)
+ @SetConsoleCursorPosition.call(@hConsoleHandle, y * 65536 + cursor_pos.x)
elsif val < 0
move_cursor_down(-val)
end
end
- def self.move_cursor_down(val)
+ def move_cursor_down(val)
if val > 0
return unless csbi = get_console_screen_buffer_info
screen_height = get_screen_size.first
y = cursor_pos.y + val
y = screen_height - 1 if y > (screen_height - 1)
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, (cursor_pos.y + val) * 65536 + cursor_pos.x)
+ @SetConsoleCursorPosition.call(@hConsoleHandle, (cursor_pos.y + val) * 65536 + cursor_pos.x)
elsif val < 0
move_cursor_up(-val)
end
end
- def self.erase_after_cursor
+ def erase_after_cursor
return unless csbi = get_console_screen_buffer_info
attributes = csbi[8, 2].unpack1('S')
cursor = csbi[4, 4].unpack1('L')
written = 0.chr * 4
- @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
- @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
+ @FillConsoleOutputCharacter.call(@hConsoleHandle, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
+ @FillConsoleOutputAttribute.call(@hConsoleHandle, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
end
- def self.scroll_down(val)
+ def scroll_down(val)
return if val < 0
return unless csbi = get_console_screen_buffer_info
buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s')
screen_height = window_bottom - window_top + 1
val = screen_height if val > screen_height
- if @@legacy_console || window_left != 0
+ if @legacy_console || window_left != 0
# unless ENABLE_VIRTUAL_TERMINAL,
# if srWindow.Left != 0 then it's conhost.exe hosted console
# and puts "\n" causes horizontal scroll. its glitch.
@@ -402,11 +404,11 @@ class Reline::Windows
scroll_rectangle = [0, val, buffer_width, buffer_lines - val].pack('s4')
destination_origin = 0 # y * 65536 + x
fill = [' '.ord, attributes].pack('SS')
- @@ScrollConsoleScreenBuffer.call(@@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill)
+ @ScrollConsoleScreenBuffer.call(@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill)
else
origin_x = x + 1
origin_y = y - window_top + 1
- @@output.write [
+ @output.write [
(origin_y != screen_height) ? "\e[#{screen_height};H" : nil,
"\n" * val,
(origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil
@@ -414,49 +416,49 @@ class Reline::Windows
end
end
- def self.clear_screen
- if @@legacy_console
+ def clear_screen
+ if @legacy_console
return unless csbi = get_console_screen_buffer_info
buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s')
fill_length = buffer_width * (window_bottom - window_top + 1)
screen_topleft = window_top * 65536
written = 0.chr * 4
- @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
- @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written)
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, screen_topleft)
+ @FillConsoleOutputCharacter.call(@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
+ @FillConsoleOutputAttribute.call(@hConsoleHandle, attributes, fill_length, screen_topleft, written)
+ @SetConsoleCursorPosition.call(@hConsoleHandle, screen_topleft)
else
- @@output.write "\e[2J" "\e[H"
+ @output.write "\e[2J" "\e[H"
end
end
- def self.set_screen_size(rows, columns)
+ def set_screen_size(rows, columns)
raise NotImplementedError
end
- def self.hide_cursor
+ def hide_cursor
size = 100
visible = 0 # 0 means false
cursor_info = [size, visible].pack('Li')
- @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
+ @SetConsoleCursorInfo.call(@hConsoleHandle, cursor_info)
end
- def self.show_cursor
+ def show_cursor
size = 100
visible = 1 # 1 means true
cursor_info = [size, visible].pack('Li')
- @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
+ @SetConsoleCursorInfo.call(@hConsoleHandle, cursor_info)
end
- def self.set_winch_handler(&handler)
- @@winch_handler = handler
+ def set_winch_handler(&handler)
+ @winch_handler = handler
end
- def self.prep
+ def prep
# do nothing
nil
end
- def self.deprep(otio)
+ def deprep(otio)
# do nothing
end
diff --git a/lib/reline/key_actor/base.rb b/lib/reline/key_actor/base.rb
index a1cd7fb2a1..194e98938c 100644
--- a/lib/reline/key_actor/base.rb
+++ b/lib/reline/key_actor/base.rb
@@ -12,8 +12,4 @@ class Reline::KeyActor::Base
def default_key_bindings
@default_key_bindings
end
-
- def reset_default_key_bindings
- @default_key_bindings.clear
- end
end
diff --git a/lib/reline/key_actor/emacs.rb b/lib/reline/key_actor/emacs.rb
index a561feee57..d7354520b0 100644
--- a/lib/reline/key_actor/emacs.rb
+++ b/lib/reline/key_actor/emacs.rb
@@ -19,7 +19,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
# 8 ^H
:em_delete_prev_char,
# 9 ^I
- :ed_unassigned,
+ :complete,
# 10 ^J
:ed_newline,
# 11 ^K
@@ -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 ^]
@@ -63,7 +63,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
# 30 ^^
:ed_unassigned,
# 31 ^_
- :ed_unassigned,
+ :undo,
# 32 SPACE
:ed_insert,
# 33 !
@@ -319,9 +319,9 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
# 158 M-^^
:ed_unassigned,
# 159 M-^_
- :em_copy_prev_word,
+ :redo,
# 160 M-SPACE
- :ed_unassigned,
+ :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..f8ccf468c6 100644
--- a/lib/reline/key_actor/vi_insert.rb
+++ b/lib/reline/key_actor/vi_insert.rb
@@ -19,7 +19,7 @@ class Reline::KeyActor::ViInsert < Reline::KeyActor::Base
# 8 ^H
:vi_delete_prev_char,
# 9 ^I
- :ed_insert,
+ :complete,
# 10 ^J
:ed_newline,
# 11 ^K
@@ -29,11 +29,11 @@ class Reline::KeyActor::ViInsert < Reline::KeyActor::Base
# 13 ^M
:ed_newline,
# 14 ^N
- :ed_insert,
+ :menu_complete,
# 15 ^O
:ed_insert,
# 16 ^P
- :ed_insert,
+ :menu_complete_backward,
# 17 ^Q
:ed_ignore,
# 18 ^R
@@ -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 e8b83e2a6e..c2f5f0622e 100644
--- a/lib/reline/line_editor.rb
+++ b/lib/reline/line_editor.rb
@@ -4,9 +4,7 @@ require 'reline/unicode'
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 +12,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,99 +32,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)
+
+ 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
- PROMPT_LIST_CACHE_TIMEOUT = 0.5
MINIMUM_SCROLLBAR_HEIGHT = 1
def initialize(config, encoding)
@config = config
@completion_append_character = ''
+ @screen_size = [0, 0] # Should be initialized with actual winsize in LineEditor#reset
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, force_recalc: false)
+ 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? && !force_recalc
+ 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?
@@ -137,24 +128,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 = "''"
@@ -178,67 +162,53 @@ 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(0)
- 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
- }
+ Reline::IOGate.set_winch_handler do
+ @resized = true
+ end
+ @old_trap = Signal.trap('INT') do
+ @interrupted = true
+ end
end
def finalize
@@ -255,56 +225,44 @@ 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
@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 = []
- @previous_rendered_dialog_y = 0
- @last_key = nil
+ @interrupted = false
@resized = false
+ @cache = {}
+ @rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0)
+ @input_lines = [[[""], 0, 0]]
+ @input_lines_position = 0
+ @undoing = false
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
@@ -315,68 +273,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\s*/, '')
+ )
+ 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
@@ -402,113 +336,242 @@ 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
- prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines)
- cursor_column = (prompt_width + @cursor) % @screen_size.last
- if @cleared
- clear_screen_buffer(prompt, prompt_list, prompt_width)
- @cleared = false
- return
+ 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, true)).first.compact
+ wrapped_prompts.map { |p| [p, ''] } + [[code_line_prompt, wrapped_lines.first]] + wrapped_lines.drop(1).map { |c| ['', c] }
+ end
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
- new_lines = whole_lines
- prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines)
- modify_lines(new_lines).each_with_index do |line, index|
- @output.write "#{prompt_list ? prompt_list[index] : prompt}#{line}\r\n"
- Reline::IOGate.erase_after_cursor
- end
- @output.flush
- clear_dialog(cursor_column)
- return
+ end
+
+ def calculate_overlay_levels(overlay_levels)
+ levels = []
+ overlay_levels.each do |x, w, l|
+ levels.fill(l, x, w)
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_trap_key(cursor_column)
- rerender_added_newline(prompt, prompt_width, prompt_list)
- @add_newline_to_end_of_buffer = false
- else
- if @just_cursor_moving and not @rerender_all
- clear_dialog_with_trap_key(cursor_column)
- rendered = just_move_cursor
- @just_cursor_moving = false
- return
- elsif @previous_line_index or new_highest_in_this != @highest_in_this
- clear_dialog_with_trap_key(cursor_column)
- rerender_changed_current_line
- @previous_line_index = nil
- rendered = true
- elsif @rerender_all
- rerender_all_lines
- @rerender_all = false
- rendered = true
+ 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_sequence}#{' ' * width}"
else
+ x, w, content = new_items[level]
+ cover_begin = base_x != 0 && new_levels[base_x - 1] == level
+ cover_end = new_levels[base_x + width] == level
+ pos = 0
+ unless x == base_x && w == width
+ content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true)
+ end
+ Reline::IOGate.move_cursor_column x + pos
+ @output.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}"
end
+ base_x += width
end
- if @is_multiline
- if finished?
- # Always rerender on finish because output_modifier_proc may return a different output.
- new_lines = whole_lines
- line = modify_lines(new_lines)[@line_index]
- clear_dialog(cursor_column)
- 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(cursor_column)
+ 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
- @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
+ @menu_info = nil # TODO: do not change state here
+ end
+
+ @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
+
+ 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
+ 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
@@ -559,21 +622,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.instance_variable_get(:@screen_size).first
+ @line_editor.screen_height
end
def preferred_dialog_height
- rest_height = @line_editor.instance_variable_get(:@rest_height)
- scroll_partial_screen = @line_editor.instance_variable_get(:@scroll_partial_screen) || 0
- [cursor_pos.y - scroll_partial_screen, rest_height, (screen_height + 6) / 5].max
+ _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
@@ -642,27 +704,6 @@ class Reline::LineEditor
end
DIALOG_DEFAULT_HEIGHT = 20
- private def render_dialog(cursor_column)
- changes = @dialogs.map do |dialog|
- old_dialog = dialog.dup
- update_each_dialog(dialog, cursor_column)
- [old_dialog, dialog]
- end
- render_dialog_changes(changes, cursor_column)
- end
-
- private def padding_space_with_escape_sequences(str, width)
- 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 range_subtract(base_ranges, subtract_ranges)
- indices = base_ranges.flat_map(&:to_a).uniq.sort - subtract_ranges.flat_map(&:to_a)
- chunks = indices.chunk_while { |a, b| a + 1 == b }
- chunks.map { |a| a.first...a.last + 1 }
- end
private def dialog_range(dialog, dialog_y)
x_range = dialog.column...dialog.column + dialog.width
@@ -670,106 +711,9 @@ class Reline::LineEditor
[x_range, y_range]
end
- private def render_dialog_changes(changes, cursor_column)
- # Collect x-coordinate range and content of previous and current dialogs for each line
- old_dialog_ranges = {}
- new_dialog_ranges = {}
- new_dialog_contents = {}
- changes.each do |old_dialog, new_dialog|
- if old_dialog.contents
- x_range, y_range = dialog_range(old_dialog, @previous_rendered_dialog_y)
- y_range.each do |y|
- (old_dialog_ranges[y] ||= []) << x_range
- end
- end
- if new_dialog.contents
- x_range, y_range = dialog_range(new_dialog, @first_line_started_from + @started_from)
- y_range.each do |y|
- (new_dialog_ranges[y] ||= []) << x_range
- (new_dialog_contents[y] ||= []) << [x_range, new_dialog.contents[y - y_range.begin]]
- end
- end
- end
- return if old_dialog_ranges.empty? && new_dialog_ranges.empty?
-
- # Calculate x-coordinate ranges to restore text that was hidden behind dialogs for each line
- ranges_to_restore = {}
- subtract_cache = {}
- old_dialog_ranges.each do |y, old_x_ranges|
- new_x_ranges = new_dialog_ranges[y] || []
- ranges = subtract_cache[[old_x_ranges, new_x_ranges]] ||= range_subtract(old_x_ranges, new_x_ranges)
- ranges_to_restore[y] = ranges if ranges.any?
- end
-
- # Create visual_lines for restoring text hidden behind dialogs
- if ranges_to_restore.any?
- lines = whole_lines
- prompt, _prompt_width, prompt_list = check_multiline_prompt(lines, force_recalc: true)
- modified_lines = modify_lines(lines, force_recalc: true)
- visual_lines = []
- modified_lines.each_with_index { |l, i|
- pr = prompt_list ? prompt_list[i] : prompt
- vl, = split_by_width(pr + l, @screen_size.last)
- vl.compact!
- visual_lines.concat(vl)
- }
- end
-
- # Clear and rerender all dialogs line by line
- Reline::IOGate.hide_cursor
- ymin, ymax = (ranges_to_restore.keys + new_dialog_ranges.keys).minmax
- scroll_partial_screen = @scroll_partial_screen || 0
- screen_y_range = scroll_partial_screen..(scroll_partial_screen + @screen_height - 1)
- ymin = ymin.clamp(screen_y_range.begin, screen_y_range.end)
- ymax = ymax.clamp(screen_y_range.begin, screen_y_range.end)
- dialog_y = @first_line_started_from + @started_from
- cursor_y = dialog_y
- if @highest_in_all <= ymax
- scroll_down(ymax - cursor_y)
- move_cursor_up(ymax - cursor_y)
- end
- (ymin..ymax).each do |y|
- move_cursor_down(y - cursor_y)
- cursor_y = y
- new_x_ranges = new_dialog_ranges[y]
- restore_ranges = ranges_to_restore[y]
- # Restore text that was hidden behind dialogs
- if restore_ranges
- line = visual_lines[y] || ''
- restore_ranges.each do |range|
- col = range.begin
- width = range.end - range.begin
- s = padding_space_with_escape_sequences(Reline::Unicode.take_range(line, col, width), width)
- Reline::IOGate.move_cursor_column(col)
- @output.write "\e[0m#{s}\e[0m"
- end
- max_column = [calculate_width(line, true), new_x_ranges&.map(&:end)&.max || 0].max
- if max_column < restore_ranges.map(&:end).max
- Reline::IOGate.move_cursor_column(max_column)
- Reline::IOGate.erase_after_cursor
- end
- end
- # Render dialog contents
- new_dialog_contents[y]&.each do |x_range, content|
- Reline::IOGate.move_cursor_column(x_range.begin)
- @output.write "\e[0m#{content}\e[0m"
- end
- end
- move_cursor_up(cursor_y - dialog_y)
- Reline::IOGate.move_cursor_column(cursor_column)
- Reline::IOGate.show_cursor
-
- @previous_rendered_dialog_y = dialog_y
- end
-
- private def update_each_dialog(dialog, cursor_column)
- if @in_pasting
- 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 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.contents = nil
dialog.trap_key = nil
@@ -809,45 +753,41 @@ class Reline::LineEditor
else
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 scrollbar_pos
- diff = (dialog.column + dialog.width) - (@screen_size.last)
+ 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
dialog.vertical_offset = dialog_render_info.pos.y + 1
end
if dialog.column < 0
dialog.column = 0
- dialog.width = @screen_size.last
+ 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|
- 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
+ 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)
- colored_content = "\e[#{bg_color}m\e[#{fg_color}m#{str}"
+ str, = Reline::Unicode.take_mbchar_range(item, 0, str_width, padding: true)
+ colored_content = "#{line_sgr}#{str}"
if scrollbar_pos
- color_seq = "\e[37m"
if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height)
- colored_content + color_seq + @full_block
+ colored_content + scrollbar_sgr + @full_block
elsif scrollbar_pos <= (i * 2) and (i * 2) < (scrollbar_pos + bar_height)
- colored_content + color_seq + @upper_half_block
+ colored_content + scrollbar_sgr + @upper_half_block
elsif scrollbar_pos <= (i * 2 + 1) and (i * 2) < (scrollbar_pos + bar_height)
- colored_content + color_seq + @lower_half_block
+ colored_content + scrollbar_sgr + @lower_half_block
else
- colored_content + color_seq + ' ' * @block_elem_width
+ colored_content + scrollbar_sgr + ' ' * @block_elem_width
end
else
colored_content
@@ -855,379 +795,20 @@ class Reline::LineEditor
end
end
- private def clear_dialog(cursor_column)
- changes = @dialogs.map do |dialog|
- old_dialog = dialog.dup
- dialog.contents = nil
- [old_dialog, dialog]
- end
- render_dialog_changes(changes, cursor_column)
- end
-
- private def clear_dialog_with_trap_key(cursor_column)
- clear_dialog(cursor_column)
- @dialogs.each do |dialog|
- dialog.trap_key = nil
- end
- 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, prompt_list)
- @buffer_of_lines[@previous_line_index] = @line
- @line = @buffer_of_lines[@line_index]
- @previous_line_index = nil
- if @in_pasting
- scroll_down(1)
- else
- lines = whole_lines
- prev_line_prompt = @prompt_proc ? prompt_list[@line_index - 1] : prompt
- prev_line_prompt_width = @prompt_proc ? calculate_width(prev_line_prompt, true) : prompt_width
- prev_line = modify_lines(lines)[@line_index - 1]
- move_cursor_up(@started_from)
- render_partial(prev_line_prompt, prev_line_prompt_width, prev_line, @first_line_started_from + @started_from, with_control: false)
- scroll_down(1)
- 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
- 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
- @cursor, @cursor_max, _, @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 + @cursor) - 1
- calculate_scroll_partial_screen(@highest_in_all, new_first_line_started_from + new_started_from)
- @previous_line_index = nil
- @line = @buffer_of_lines[@line_index]
- if @rerender_all
- rerender_all_lines
- @rerender_all = false
- true
- else
- @first_line_started_from = new_first_line_started_from
- @started_from = new_started_from
- 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
- new_lines = whole_lines
- 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
- else
- scroll_down(1)
- end
- rendered_height += height
- else
- rendered_height += height - 1
- 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, force_recalc: false)
- return before if !force_recalc && (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
- 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
+ before.map { |l| Reline::Unicode.escape_for_print(l) }
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)
@@ -1255,7 +836,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])
@@ -1276,9 +857,9 @@ class Reline::LineEditor
[target, preposing, completed, postposing]
end
- private def complete(list, just_show_list = false)
+ private def perform_completion(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)
@@ -1305,100 +886,80 @@ class Reline::LineEditor
@completion_state = CompletionState::PERFECT_MATCH
else
@completion_state = CompletionState::MENU_WITH_PERFECT_MATCH
+ perform_completion(list, true) if @config.show_all_if_ambiguous
end
@perfect_matched = completed
else
@completion_state = CompletionState::MENU
+ perform_completion(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).split("\n")[@line_index] || String.new(encoding: @encoding)
- line_to_pointer = (preposing + completed + completion_append_character.to_s).split("\n").last || String.new(encoding: @encoding)
- @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
+ method_obj = method(@vi_waiting_operator)
+ wrap_method_call(@vi_waiting_operator, method_obj, 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
@@ -1415,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
@@ -1434,11 +995,33 @@ 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
+ method_obj = method(@vi_waiting_operator)
+ wrap_method_call(@vi_waiting_operator, method_obj, 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)
@@ -1450,7 +1033,6 @@ class Reline::LineEditor
end
@kill_ring.process
if @vi_arg
- @rerender_al = true
@vi_arg = nil
end
elsif @vi_arg
@@ -1461,8 +1043,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
@@ -1470,13 +1050,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)
@@ -1492,11 +1068,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
- end
@multibyte_buffer << key.combined_char
if @multibyte_buffer.size > 1
if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding?
@@ -1510,97 +1081,110 @@ 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
+ save_old_buffer
@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?
+ process_insert(force: true)
if @first_char
- @line = nil
+ @eof = true
end
finish
return
end
- old_line = @line.dup
@first_char = 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)
- 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
- end
- elsif Symbol === key.char and respond_to?(key.char, true)
+ @completion_occurs = false
+
+ if key.char.is_a?(Symbol)
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
+
+ push_input_lines unless @undoing
+ @undoing = false
+
+ if @in_pasting
+ clear_dialogs
+ return
+ end
+
+ modified = @old_buffer_of_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 save_old_buffer
+ @old_buffer_of_lines = @buffer_of_lines.dup
+ end
+
+ def push_input_lines
+ if @old_buffer_of_lines == @buffer_of_lines
+ @input_lines[@input_lines_position] = [@buffer_of_lines.dup, @byte_pointer, @line_index]
else
- @just_cursor_moving = false
+ @input_lines = @input_lines[0..@input_lines_position]
+ @input_lines_position += 1
+ @input_lines.push([@buffer_of_lines.dup, @byte_pointer, @line_index])
end
- if @is_multiline and @auto_indent_proc and not simplified_rendering?
- process_auto_indent
+ trim_input_lines
+ end
+
+ MAX_INPUT_LINES = 100
+ def trim_input_lines
+ if @input_lines.size > MAX_INPUT_LINES
+ @input_lines.shift
+ @input_lines_position -= 1
+ end
+ 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 wrapped_cursor_y >= screen_scroll_top + screen_height
+ @scroll_partial_screen = wrapped_cursor_y - screen_height + 1
end
end
@@ -1634,43 +1218,52 @@ 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
- 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
-
- 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
- end
- new_lines = whole_lines
- new_indent = @auto_indent_proc.(new_lines, @line_index, @byte_pointer, @check_new_auto_indent)
- if new_indent&.>= 0
- md = new_lines[@line_index].match(/\A */)
- prev_indent = md[0].count(' ')
- if @check_new_auto_indent
- line = @buffer_of_lines[@line_index] = ' ' * new_indent + @buffer_of_lines[@line_index].lstrip
- @cursor = new_indent
- @cursor_max = calculate_width(line)
- @byte_pointer = new_indent
- else
- @line = ' ' * new_indent + @line.lstrip
- @cursor += new_indent - prev_indent
- @cursor_max = calculate_width(@line)
- @byte_pointer += new_indent - prev_indent
- end
+ private def process_auto_indent(line_index = @line_index, cursor_dependent: true, add_newline: false)
+ return if @in_pasting
+ return unless @auto_indent_proc
+
+ 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
- @check_new_auto_indent = false
+ 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
+ calculate_nearest_cursor(cursor)
+ end
+ process_auto_indent
+ end
+
+ def set_current_lines(lines, byte_pointer = nil, line_index = 0)
+ cursor = current_byte_pointer_cursor
+ @buffer_of_lines = lines
+ @line_index = line_index
+ if byte_pointer
+ @byte_pointer = byte_pointer
+ else
+ calculate_nearest_cursor(cursor)
+ end
+ process_auto_indent
end
def retrieve_completion_block(set_completion_quote_character = false)
@@ -1684,7 +1277,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
@@ -1692,7 +1285,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
@@ -1714,15 +1307,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)
@@ -1733,126 +1326,93 @@ 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
- 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
+ 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_pasted_text(text)
+ save_old_buffer
+ pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer)
+ post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..)
+ lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1)
+ lines << '' if lines.empty?
+ @buffer_of_lines[@line_index, 1] = lines
+ @line_index += lines.size - 1
+ @byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize
+ push_input_lines
+ 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 = @previous_line_index || @line_index
- temp_lines = @buffer_of_lines.dup
- temp_lines[index] = @line
- temp_lines
+ @buffer_of_lines.dup
end
def whole_buffer
- if @buffer_of_lines.size == 1 and @line.nil?
- nil
- else
- whole_lines.join("\n")
- end
+ whole_lines.join("\n")
end
def finished?
@@ -1861,7 +1421,6 @@ class Reline::LineEditor
def finish
@finished = true
- @rerender_all = true
@config.reset
end
@@ -1892,33 +1451,56 @@ class Reline::LineEditor
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 complete(_key)
+ return 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
+ perform_completion(result, false)
+ end
+ end
+ end
+
+ private def completion_journey_move(direction)
+ return if @config.disable_completion
+
+ process_insert(force: true)
+ @completion_state = CompletionState::NORMAL
+ @completion_occurs = move_completed_list(direction)
+ end
+
+ private def menu_complete(_key)
+ completion_journey_move(:down)
+ end
+
+ private def menu_complete_backward(_key)
+ completion_journey_move(:up)
+ end
+
+ private def completion_journey_up(_key)
+ completion_journey_move(:up) if @config.autocompletion
+ end
+
# 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
@@ -1936,9 +1518,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)
@@ -1946,7 +1525,6 @@ class Reline::LineEditor
return
end
str = key
- bytesize = key.bytesize
else
begin
key.chr.encode(Encoding::UTF_8)
@@ -1954,7 +1532,6 @@ class Reline::LineEditor
return
end
str = key.chr
- bytesize = 1
end
if @in_pasting
@continuous_insertion_buffer << str
@@ -1962,28 +1539,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
@@ -2005,18 +1562,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
@@ -2025,19 +1575,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
@@ -2045,157 +1588,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
@@ -2207,67 +1702,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
}
@@ -2283,199 +1752,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
@@ -2500,40 +1865,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
@@ -2543,23 +1897,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.
@@ -2567,11 +1921,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
@@ -2581,50 +1933,35 @@ 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 @line.empty? and (not @is_multiline or @buffer_of_lines.size == 1) and key == "\C-d".ord
- @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)
+ perform_completion(result, true)
end
end
end
@@ -2632,162 +1969,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
@@ -2815,10 +2126,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
@@ -2826,38 +2136,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
@@ -2865,50 +2169,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
@@ -2923,78 +2216,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
@@ -3005,18 +2301,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
@@ -3027,54 +2320,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
@@ -3082,11 +2346,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
@@ -3110,43 +2372,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
}
@@ -3169,7 +2421,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
@@ -3189,21 +2441,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
@@ -3226,7 +2473,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
@@ -3246,26 +2493,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
@@ -3279,11 +2519,36 @@ 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
+
+ private def undo(_key)
+ @undoing = true
+
+ return if @input_lines_position <= 0
+
+ @input_lines_position -= 1
+ target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
+ set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
+ end
+
+ private def redo(_key)
+ @undoing = true
+
+ return if @input_lines_position >= @input_lines.size - 1
+
+ @input_lines_position += 1
+ target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
+ set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
+ 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 2cfa32b9f7..6885a0c6be 100644
--- a/lib/reline/terminfo.rb
+++ b/lib/reline/terminfo.rb
@@ -80,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
@@ -148,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 0a7f59cf06..d7460d6d4a 100644
--- a/lib/reline/unicode.rb
+++ b/lib/reline/unicode.rb
@@ -41,33 +41,15 @@ class Reline::Unicode
OSC_REGEXP = /\e\]\d+(?:;[^;\a\e]+)*(?:\a|\e\\)/
WIDTH_SCANNER = /\G(?:(#{NON_PRINTING_START})|(#{NON_PRINTING_END})|(#{CSI_REGEXP})|(#{OSC_REGEXP})|(\X))/o
- 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|
- escaped = EscapedPairs[gr.ord]
- if escaped && gr != -"\n" && gr != -"\t"
- escaped
- else
+ case gr
+ when -"\n"
gr
+ when -"\t"
+ -' '
+ else
+ EscapedPairs[gr.ord] || gr
end
}.join
end
@@ -148,10 +130,10 @@ 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
seq = String.new(encoding: encoding)
@@ -165,7 +147,13 @@ class Reline::Unicode
lines.last << NON_PRINTING_END
when csi
lines.last << csi
- seq << 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
@@ -193,32 +181,78 @@ class Reline::Unicode
# Take a chunk of a String cut by width with escape sequences.
def self.take_range(str, start_col, max_width)
+ take_mbchar_range(str, start_col, max_width).first
+ end
+
+ def self.take_mbchar_range(str, start_col, width, cover_begin: false, cover_end: false, padding: false)
chunk = String.new(encoding: str.encoding)
+
+ end_col = start_col + width
total_width = 0
rest = str.encode(Encoding::UTF_8)
in_zero_width = false
+ chunk_start_col = nil
+ chunk_end_col = nil
+ has_csi = false
rest.scan(WIDTH_SCANNER) do |non_printing_start, non_printing_end, csi, osc, gc|
case
when non_printing_start
in_zero_width = true
+ chunk << NON_PRINTING_START
when non_printing_end
in_zero_width = false
+ chunk << NON_PRINTING_END
when csi
+ has_csi = true
chunk << csi
when osc
chunk << osc
when gc
if in_zero_width
chunk << gc
+ next
+ end
+
+ mbchar_width = get_mbchar_width(gc)
+ prev_width = total_width
+ total_width += mbchar_width
+
+ if (cover_begin || padding ? total_width <= start_col : prev_width < start_col)
+ # Current character haven't reached start_col yet
+ next
+ elsif padding && !cover_begin && prev_width < start_col && start_col < total_width
+ # Add preceding padding. This padding might have background color.
+ chunk << ' '
+ chunk_start_col ||= start_col
+ chunk_end_col = total_width
+ next
+ elsif (cover_end ? prev_width < end_col : total_width <= end_col)
+ # Current character is in the range
+ chunk << gc
+ chunk_start_col ||= prev_width
+ chunk_end_col = total_width
+ break if total_width >= end_col
else
- mbchar_width = get_mbchar_width(gc)
- total_width += mbchar_width
- break if (start_col + max_width) < total_width
- chunk << gc if start_col < total_width
+ # Current character exceeds end_col
+ if padding && end_col < total_width
+ # Add succeeding padding. This padding might have background color.
+ chunk << ' '
+ chunk_start_col ||= prev_width
+ chunk_end_col = end_col
+ end
+ break
end
end
end
- chunk
+ chunk_start_col ||= start_col
+ chunk_end_col ||= start_col
+ if padding && chunk_end_col < end_col
+ # Append padding. This padding should not include background color.
+ chunk << "\e[0m" if has_csi
+ chunk << ' ' * (end_col - chunk_end_col)
+ chunk_end_col = end_col
+ end
+ [chunk, chunk_start_col, chunk_end_col - chunk_start_col]
end
def self.get_next_mbchar_size(line, byte_pointer)
diff --git a/lib/reline/unicode/east_asian_width.rb b/lib/reline/unicode/east_asian_width.rb
index 97dfec4c52..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}
diff --git a/lib/reline/version.rb b/lib/reline/version.rb
index abb200526c..680e5a9cca 100644
--- a/lib/reline/version.rb
+++ b/lib/reline/version.rb
@@ -1,3 +1,3 @@
module Reline
- VERSION = '0.3.5'
+ VERSION = '0.5.8'
end