diff options
136 files changed, 6571 insertions, 6696 deletions
diff --git a/doc/irb/indexes.md b/doc/irb/indexes.md index 9659db8c0b..24a67b9698 100644 --- a/doc/irb/indexes.md +++ b/doc/irb/indexes.md @@ -165,9 +165,6 @@ for each entry that is pre-defined, the initial value is given: - `:RC`: Whether a {configuration file}[rdoc-ref:IRB@Configuration+File] was found and interpreted; initial value: `true` if a configuration file was found, `false` otherwise. -- `:RC_NAME_GENERATOR`: \Proc to generate paths of potential - {configuration files}[rdoc-ref:IRB@Configuration+File]; - initial value: `=> #<Proc:0x000055f9bebfed80 /var/lib/gems/3.0.0/gems/irb-1.8.3/lib/irb/init.rb:401>`. - `:SAVE_HISTORY`: Number of commands to save in {input command history}[rdoc-ref:IRB@Input+Command+History]; initial value: `1000`. diff --git a/lib/irb.rb b/lib/irb.rb index 006b52bec5..b3435c257e 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -1,5 +1,6 @@ -# frozen_string_literal: false -# +# frozen_string_literal: true + +# :markup: markdown # irb.rb - irb main module # by Keiju ISHITSUKA(keiju@ruby-lang.org) # @@ -9,7 +10,7 @@ require "reline" require_relative "irb/init" require_relative "irb/context" -require_relative "irb/extend-command" +require_relative "irb/default_commands" require_relative "irb/ruby-lex" require_relative "irb/statement" @@ -22,546 +23,553 @@ require_relative "irb/easter-egg" require_relative "irb/debug" require_relative "irb/pager" -# == \IRB +# ## IRB # -# \Module \IRB ("Interactive Ruby") provides a shell-like interface -# that supports user interaction with the Ruby interpreter. +# Module IRB ("Interactive Ruby") provides a shell-like interface that supports +# user interaction with the Ruby interpreter. # -# It operates as a <i>read-eval-print loop</i> -# ({REPL}[https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop]) +# It operates as a *read-eval-print loop* +# ([REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)) # that: # -# - <b>_Reads_</b> each character as you type. -# You can modify the \IRB context to change the way input works. -# See {Input}[rdoc-ref:IRB@Input]. -# - <b>_Evaluates_</b> the code each time it has read a syntactically complete passage. -# - <b>_Prints_</b> after evaluating. -# You can modify the \IRB context to change the way output works. -# See {Output}[rdoc-ref:IRB@Output]. +# * ***Reads*** each character as you type. You can modify the IRB context to +# change the way input works. See [Input](rdoc-ref:IRB@Input). +# * ***Evaluates*** the code each time it has read a syntactically complete +# passage. +# * ***Prints*** after evaluating. You can modify the IRB context to change +# the way output works. See [Output](rdoc-ref:IRB@Output). +# # # Example: # -# $ irb -# irb(main):001> File.basename(Dir.pwd) -# => "irb" -# irb(main):002> Dir.entries('.').size -# => 25 -# irb(main):003* Dir.entries('.').select do |entry| -# irb(main):004* entry.start_with?('R') -# irb(main):005> end -# => ["README.md", "Rakefile"] +# $ irb +# irb(main):001> File.basename(Dir.pwd) +# => "irb" +# irb(main):002> Dir.entries('.').size +# => 25 +# irb(main):003* Dir.entries('.').select do |entry| +# irb(main):004* entry.start_with?('R') +# irb(main):005> end +# => ["README.md", "Rakefile"] +# +# The typed input may also include [\IRB-specific +# commands](rdoc-ref:IRB@IRB-Specific+Commands). # -# The typed input may also include -# {\IRB-specific commands}[rdoc-ref:IRB@IRB-Specific+Commands]. +# As seen above, you can start IRB by using the shell command `irb`. # -# As seen above, you can start \IRB by using the shell command +irb+. +# You can stop an IRB session by typing command `exit`: # -# You can stop an \IRB session by typing command +exit+: +# irb(main):006> exit +# $ # -# irb(main):006> exit -# $ +# At that point, IRB calls any hooks found in array `IRB.conf[:AT_EXIT]`, then +# exits. # -# At that point, \IRB calls any hooks found in array <tt>IRB.conf[:AT_EXIT]</tt>, -# then exits. +# ## Startup # -# == Startup +# At startup, IRB: # -# At startup, \IRB: +# 1. Interprets (as Ruby code) the content of the [configuration +# file](rdoc-ref:IRB@Configuration+File) (if given). +# 2. Constructs the initial session context from [hash +# IRB.conf](rdoc-ref:IRB@Hash+IRB.conf) and from default values; the hash +# content may have been affected by [command-line +# options](rdoc-ref:IB@Command-Line+Options), and by direct assignments in +# the configuration file. +# 3. Assigns the context to variable `conf`. +# 4. Assigns command-line arguments to variable `ARGV`. +# 5. Prints the [prompt](rdoc-ref:IRB@Prompt+and+Return+Formats). +# 6. Puts the content of the [initialization +# script](rdoc-ref:IRB@Initialization+Script) onto the IRB shell, just as if +# it were user-typed commands. # -# 1. Interprets (as Ruby code) the content of the -# {configuration file}[rdoc-ref:IRB@Configuration+File] (if given). -# 1. Constructs the initial session context -# from {hash IRB.conf}[rdoc-ref:IRB@Hash+IRB.conf] and from default values; -# the hash content may have been affected -# by {command-line options}[rdoc-ref:IB@Command-Line+Options], -# and by direct assignments in the configuration file. -# 1. Assigns the context to variable +conf+. -# 1. Assigns command-line arguments to variable <tt>ARGV</tt>. -# 1. Prints the {prompt}[rdoc-ref:IRB@Prompt+and+Return+Formats]. -# 1. Puts the content of the -# {initialization script}[rdoc-ref:IRB@Initialization+Script] -# onto the \IRB shell, just as if it were user-typed commands. # -# === The Command Line +# ### The Command Line # -# On the command line, all options precede all arguments; -# the first item that is not recognized as an option is treated as an argument, -# as are all items that follow. +# On the command line, all options precede all arguments; the first item that is +# not recognized as an option is treated as an argument, as are all items that +# follow. # -# ==== Command-Line Options +# #### Command-Line Options # -# Many command-line options affect entries in hash <tt>IRB.conf</tt>, -# which in turn affect the initial configuration of the \IRB session. +# Many command-line options affect entries in hash `IRB.conf`, which in turn +# affect the initial configuration of the IRB session. # # Details of the options are described in the relevant subsections below. # -# A cursory list of the \IRB command-line options -# may be seen in the {help message}[https://raw.githubusercontent.com/ruby/irb/master/lib/irb/lc/help-message], -# which is also displayed if you use command-line option <tt>--help</tt>. +# A cursory list of the IRB command-line options may be seen in the [help +# message](https://raw.githubusercontent.com/ruby/irb/master/lib/irb/lc/help-message), +# which is also displayed if you use command-line option `--help`. # # If you are interested in a specific option, consult the -# {index}[rdoc-ref:doc/irb/indexes.md@Index+of+Command-Line+Options]. +# [index](rdoc-ref:doc/irb/indexes.md@Index+of+Command-Line+Options). # -# ==== Command-Line Arguments +# #### Command-Line Arguments # -# Command-line arguments are passed to \IRB in array +ARGV+: +# Command-line arguments are passed to IRB in array `ARGV`: # -# $ irb --noscript Foo Bar Baz -# irb(main):001> ARGV -# => ["Foo", "Bar", "Baz"] -# irb(main):002> exit -# $ +# $ irb --noscript Foo Bar Baz +# irb(main):001> ARGV +# => ["Foo", "Bar", "Baz"] +# irb(main):002> exit +# $ # -# Command-line option <tt>--</tt> causes everything that follows -# to be treated as arguments, even those that look like options: +# Command-line option `--` causes everything that follows to be treated as +# arguments, even those that look like options: # -# $ irb --noscript -- --noscript -- Foo Bar Baz -# irb(main):001> ARGV -# => ["--noscript", "--", "Foo", "Bar", "Baz"] -# irb(main):002> exit -# $ +# $ irb --noscript -- --noscript -- Foo Bar Baz +# irb(main):001> ARGV +# => ["--noscript", "--", "Foo", "Bar", "Baz"] +# irb(main):002> exit +# $ # -# === Configuration File +# ### Configuration File # -# You can initialize \IRB via a <i>configuration file</i>. +# You can initialize IRB via a *configuration file*. # -# If command-line option <tt>-f</tt> is given, -# no configuration file is looked for. +# If command-line option `-f` is given, no configuration file is looked for. # -# Otherwise, \IRB reads and interprets a configuration file -# if one is available. +# Otherwise, IRB reads and interprets a configuration file if one is available. # # The configuration file can contain any Ruby code, and can usefully include # user code that: # -# - Can then be debugged in \IRB. -# - Configures \IRB itself. -# - Requires or loads files. +# * Can then be debugged in IRB. +# * Configures IRB itself. +# * Requires or loads files. +# # # The path to the configuration file is the first found among: # -# - The value of variable <tt>$IRBRC</tt>, if defined. -# - The value of variable <tt>$XDG_CONFIG_HOME/irb/irbrc</tt>, if defined. -# - File <tt>$HOME/.irbrc</tt>, if it exists. -# - File <tt>$HOME/.config/irb/irbrc</tt>, if it exists. -# - File +.config/irb/irbrc+ in the current directory, if it exists. -# - File +.irbrc+ in the current directory, if it exists. -# - File +irb.rc+ in the current directory, if it exists. -# - File +_irbrc+ in the current directory, if it exists. -# - File <tt>$irbrc</tt> in the current directory, if it exists. +# * The value of variable `$IRBRC`, if defined. +# * The value of variable `$XDG_CONFIG_HOME/irb/irbrc`, if defined. +# * File `$HOME/.irbrc`, if it exists. +# * File `$HOME/.config/irb/irbrc`, if it exists. +# * File `.irbrc` in the current directory, if it exists. +# * File `irb.rc` in the current directory, if it exists. +# * File `_irbrc` in the current directory, if it exists. +# * File `$irbrc` in the current directory, if it exists. +# # # If the search fails, there is no configuration file. # -# If the search succeeds, the configuration file is read as Ruby code, -# and so can contain any Ruby programming you like. +# If the search succeeds, the configuration file is read as Ruby code, and so +# can contain any Ruby programming you like. +# +# Method `conf.rc?` returns `true` if a configuration file was read, `false` +# otherwise. Hash entry `IRB.conf[:RC]` also contains that value. # -# \Method <tt>conf.rc?</tt> returns +true+ if a configuration file was read, -# +false+ otherwise. -# \Hash entry <tt>IRB.conf[:RC]</tt> also contains that value. +# ### Hash `IRB.conf` # -# === \Hash <tt>IRB.conf</tt> +# The initial entries in hash `IRB.conf` are determined by: # -# The initial entries in hash <tt>IRB.conf</tt> are determined by: +# * Default values. +# * Command-line options, which may override defaults. +# * Direct assignments in the configuration file. # -# - Default values. -# - Command-line options, which may override defaults. -# - Direct assignments in the configuration file. # -# You can see the hash by typing <tt>IRB.conf</tt>. +# You can see the hash by typing `IRB.conf`. # -# Details of the entries' meanings are described in the relevant subsections below. +# Details of the entries' meanings are described in the relevant subsections +# below. # # If you are interested in a specific entry, consult the -# {index}[rdoc-ref:doc/irb/indexes.md@Index+of+IRB.conf+Entries]. +# [index](rdoc-ref:doc/irb/indexes.md@Index+of+IRB.conf+Entries). +# +# ### Notes on Initialization Precedence # -# === Notes on Initialization Precedence +# * Any conflict between an entry in hash `IRB.conf` and a command-line option +# is resolved in favor of the hash entry. +# * Hash `IRB.conf` affects the context only once, when the configuration file +# is interpreted; any subsequent changes to it do not affect the context and +# are therefore essentially meaningless. # -# - Any conflict between an entry in hash <tt>IRB.conf</tt> and a command-line option -# is resolved in favor of the hash entry. -# - \Hash <tt>IRB.conf</tt> affects the context only once, -# when the configuration file is interpreted; -# any subsequent changes to it do not affect the context -# and are therefore essentially meaningless. # -# === Initialization Script +# ### Initialization Script # -# By default, the first command-line argument (after any options) -# is the path to a Ruby initialization script. +# By default, the first command-line argument (after any options) is the path to +# a Ruby initialization script. # -# \IRB reads the initialization script and puts its content onto the \IRB shell, +# IRB reads the initialization script and puts its content onto the IRB shell, # just as if it were user-typed commands. # -# Command-line option <tt>--noscript</tt> causes the first command-line argument -# to be treated as an ordinary argument (instead of an initialization script); -# <tt>--script</tt> is the default. +# Command-line option `--noscript` causes the first command-line argument to be +# treated as an ordinary argument (instead of an initialization script); +# `--script` is the default. # -# == Input +# ## Input # -# This section describes the features that allow you to change -# the way \IRB input works; -# see also {Input and Output}[rdoc-ref:IRB@Input+and+Output]. +# This section describes the features that allow you to change the way IRB input +# works; see also [Input and Output](rdoc-ref:IRB@Input+and+Output). # -# === Input Command History +# ### Input Command History # -# By default, \IRB stores a history of up to 1000 input commands -# in file <tt>~/.irb_history</tt> -# (or, if a {configuration file}[rdoc-ref:IRB@Configuration+File] -# is found, in file +.irb_history+ -# inin the same directory as that file). +# By default, IRB stores a history of up to 1000 input commands in a file named +# `.irb_history`. The history file will be in the same directory as the +# [configuration file](rdoc-ref:IRB@Configuration+File) if one is found, or in +# `~/` otherwise. # -# A new \IRB session creates the history file if it does not exist, -# and appends to the file if it does exist. +# A new IRB session creates the history file if it does not exist, and appends +# to the file if it does exist. # # You can change the filepath by adding to your configuration file: -# <tt>IRB.conf[:HISTORY_FILE] = _filepath_</tt>, -# where _filepath_ is a string filepath. +# `IRB.conf[:HISTORY_FILE] = *filepath*`, where *filepath* is a string filepath. # -# During the session, method <tt>conf.history_file</tt> returns the filepath, -# and method <tt>conf.history_file = <i>new_filepath</i></tt> -# copies the history to the file at <i>new_filepath</i>, -# which becomes the history file for the session. +# During the session, method `conf.history_file` returns the filepath, and +# method `conf.history_file = *new_filepath*` copies the history to the file at +# *new_filepath*, which becomes the history file for the session. # -# You can change the number of commands saved by adding to your configuration file: -# <tt>IRB.conf[:SAVE_HISTORY] = _n_</tt>, -# where _n_ is one of: +# You can change the number of commands saved by adding to your configuration +# file: `IRB.conf[:SAVE_HISTORY] = *n*`, wheHISTORY_FILEre *n* is one of: # -# - Positive integer: the number of commands to be saved, -# - Zero: all commands are to be saved. -# - +nil+: no commands are to be saved,. +# * Positive integer: the number of commands to be saved, +# * Zero: all commands are to be saved. +# * `nil`: no commands are to be saved,. # -# During the session, you can use -# methods <tt>conf.save_history</tt> or <tt>conf.save_history=</tt> -# to retrieve or change the count. # -# === Command Aliases +# During the session, you can use methods `conf.save_history` or +# `conf.save_history=` to retrieve or change the count. # -# By default, \IRB defines several command aliases: +# ### Command Aliases # -# irb(main):001> conf.command_aliases -# => {:"$"=>:show_source, :"@"=>:whereami} +# By default, IRB defines several command aliases: +# +# irb(main):001> conf.command_aliases +# => {:"$"=>:show_source, :"@"=>:whereami} # # You can change the initial aliases in the configuration file with: # -# IRB.conf[:COMMAND_ALIASES] = {foo: :show_source, bar: :whereami} +# IRB.conf[:COMMAND_ALIASES] = {foo: :show_source, bar: :whereami} # -# You can replace the current aliases at any time -# with configuration method <tt>conf.command_aliases=</tt>; -# Because <tt>conf.command_aliases</tt> is a hash, -# you can modify it. +# You can replace the current aliases at any time with configuration method +# `conf.command_aliases=`; Because `conf.command_aliases` is a hash, you can +# modify it. # -# === End-of-File +# ### End-of-File # -# By default, <tt>IRB.conf[:IGNORE_EOF]</tt> is +false+, -# which means that typing the end-of-file character <tt>Ctrl-D</tt> -# causes the session to exit. +# By default, `IRB.conf[:IGNORE_EOF]` is `false`, which means that typing the +# end-of-file character `Ctrl-D` causes the session to exit. # -# You can reverse that behavior by adding <tt>IRB.conf[:IGNORE_EOF] = true</tt> -# to the configuration file. +# You can reverse that behavior by adding `IRB.conf[:IGNORE_EOF] = true` to the +# configuration file. # -# During the session, method <tt>conf.ignore_eof?</tt> returns the setting, -# and method <tt>conf.ignore_eof = _boolean_</tt> sets it. +# During the session, method `conf.ignore_eof?` returns the setting, and method +# `conf.ignore_eof = *boolean*` sets it. # -# === SIGINT +# ### SIGINT # -# By default, <tt>IRB.conf[:IGNORE_SIGINT]</tt> is +true+, -# which means that typing the interrupt character <tt>Ctrl-C</tt> -# causes the session to exit. +# By default, `IRB.conf[:IGNORE_SIGINT]` is `true`, which means that typing the +# interrupt character `Ctrl-C` causes the session to exit. # -# You can reverse that behavior by adding <tt>IRB.conf[:IGNORE_SIGING] = false</tt> -# to the configuration file. +# You can reverse that behavior by adding `IRB.conf[:IGNORE_SIGING] = false` to +# the configuration file. # -# During the session, method <tt>conf.ignore_siging?</tt> returns the setting, -# and method <tt>conf.ignore_sigint = _boolean_</tt> sets it. +# During the session, method `conf.ignore_siging?` returns the setting, and +# method `conf.ignore_sigint = *boolean*` sets it. # -# === Automatic Completion +# ### Automatic Completion # -# By default, \IRB enables -# {automatic completion}[https://en.wikipedia.org/wiki/Autocomplete#In_command-line_interpreters]: +# By default, IRB enables [automatic +# completion](https://en.wikipedia.org/wiki/Autocomplete#In_command-line_interpr +# eters): # # You can disable it by either of these: # -# - Adding <tt>IRB.conf[:USE_AUTOCOMPLETE] = false</tt> to the configuration file. -# - Giving command-line option <tt>--noautocomplete</tt> -# (<tt>--autocomplete</tt> is the default). +# * Adding `IRB.conf[:USE_AUTOCOMPLETE] = false` to the configuration file. +# * Giving command-line option `--noautocomplete` (`--autocomplete` is the +# default). +# # -# \Method <tt>conf.use_autocomplete?</tt> returns +true+ -# if automatic completion is enabled, +false+ otherwise. +# Method `conf.use_autocomplete?` returns `true` if automatic completion is +# enabled, `false` otherwise. # # The setting may not be changed during the session. # -# === Automatic Indentation +# ### Automatic Indentation # -# By default, \IRB automatically indents lines of code to show structure -# (e.g., it indent the contents of a block). +# By default, IRB automatically indents lines of code to show structure (e.g., +# it indent the contents of a block). # -# The current setting is returned -# by the configuration method <tt>conf.auto_indent_mode</tt>. +# The current setting is returned by the configuration method +# `conf.auto_indent_mode`. # -# The default initial setting is +true+: +# The default initial setting is `true`: # -# irb(main):001> conf.auto_indent_mode -# => true -# irb(main):002* Dir.entries('.').select do |entry| -# irb(main):003* entry.start_with?('R') -# irb(main):004> end -# => ["README.md", "Rakefile"] +# irb(main):001> conf.auto_indent_mode +# => true +# irb(main):002* Dir.entries('.').select do |entry| +# irb(main):003* entry.start_with?('R') +# irb(main):004> end +# => ["README.md", "Rakefile"] # -# You can change the initial setting in the -# configuration file with: +# You can change the initial setting in the configuration file with: # -# IRB.conf[:AUTO_INDENT] = false +# IRB.conf[:AUTO_INDENT] = false # -# Note that the _current_ setting <i>may not</i> be changed in the \IRB session. +# Note that the *current* setting *may not* be changed in the IRB session. # -# === Input \Method +# ### Input Method # -# The \IRB input method determines how command input is to be read; -# by default, the input method for a session is IRB::RelineInputMethod. +# The IRB input method determines how command input is to be read; by default, +# the input method for a session is IRB::RelineInputMethod. Unless the +# value of the TERM environment variable is 'dumb', in which case the +# most simplistic input method is used. # # You can set the input method by: # -# - Adding to the configuration file: +# * Adding to the configuration file: # -# - <tt>IRB.conf[:USE_SINGLELINE] = true</tt> -# or <tt>IRB.conf[:USE_MULTILINE]= false</tt> -# sets the input method to IRB::ReadlineInputMethod. -# - <tt>IRB.conf[:USE_SINGLELINE] = false</tt> -# or <tt>IRB.conf[:USE_MULTILINE] = true</tt> -# sets the input method to IRB::RelineInputMethod. +# * `IRB.conf[:USE_SINGLELINE] = true` or `IRB.conf[:USE_MULTILINE]= +# false` sets the input method to IRB::ReadlineInputMethod. +# * `IRB.conf[:USE_SINGLELINE] = false` or `IRB.conf[:USE_MULTILINE] = +# true` sets the input method to IRB::RelineInputMethod. # -# - Giving command-line options: # -# - <tt>--singleline</tt> -# or <tt>--nomultiline</tt> -# sets the input method to IRB::ReadlineInputMethod. -# - <tt>--nosingleline</tt> -# or <tt>--multiline/tt> -# sets the input method to IRB::RelineInputMethod. +# * Giving command-line options: # -# \Method <tt>conf.use_multiline?</tt> -# and its synonym <tt>conf.use_reline</tt> return: +# * `--singleline` or `--nomultiline` sets the input method to +# IRB::ReadlineInputMethod. +# * `--nosingleline` or `--multiline` sets the input method to +# IRB::RelineInputMethod. +# * `--nosingleline` together with `--nomultiline` sets the +# input to IRB::StdioInputMethod. # -# - +true+ if option <tt>--multiline</tt> was given. -# - +false+ if option <tt>--nomultiline</tt> was given. -# - +nil+ if neither was given. # -# \Method <tt>conf.use_singleline?</tt> -# and its synonym <tt>conf.use_readline</tt> return: +# Method `conf.use_multiline?` and its synonym `conf.use_reline` return: # -# - +true+ if option <tt>--singleline</tt> was given. -# - +false+ if option <tt>--nosingleline</tt> was given. -# - +nil+ if neither was given. +# * `true` if option `--multiline` was given. +# * `false` if option `--nomultiline` was given. +# * `nil` if neither was given. # -# == Output # -# This section describes the features that allow you to change -# the way \IRB output works; -# see also {Input and Output}[rdoc-ref:IRB@Input+and+Output]. +# Method `conf.use_singleline?` and its synonym `conf.use_readline` return: # -# === Return-Value Printing (Echoing) +# * `true` if option `--singleline` was given. +# * `false` if option `--nosingleline` was given. +# * `nil` if neither was given. # -# By default, \IRB prints (echoes) the values returned by all input commands. +# +# ## Output +# +# This section describes the features that allow you to change the way IRB +# output works; see also [Input and Output](rdoc-ref:IRB@Input+and+Output). +# +# ### Return-Value Printing (Echoing) +# +# By default, IRB prints (echoes) the values returned by all input commands. # # You can change the initial behavior and suppress all echoing by: # -# - Adding to the configuration file: <tt>IRB.conf[:ECHO] = false</tt>. -# (The default value for this entry is +niL+, which means the same as +true+.) -# - Giving command-line option <tt>--noecho</tt>. -# (The default is <tt>--echo</tt>.) +# * Adding to the configuration file: `IRB.conf[:ECHO] = false`. (The default +# value for this entry is `nil`, which means the same as `true`.) +# * Giving command-line option `--noecho`. (The default is `--echo`.) +# +# +# During the session, you can change the current setting with configuration +# method `conf.echo=` (set to `true` or `false`). # -# During the session, you can change the current setting -# with configuration method <tt>conf.echo=</tt> (set to +true+ or +false+). +# As stated above, by default IRB prints the values returned by all input +# commands; but IRB offers special treatment for values returned by assignment +# statements, which may be: # -# As stated above, by default \IRB prints the values returned by all input commands; -# but \IRB offers special treatment for values returned by assignment statements, -# which may be: +# * Printed with truncation (to fit on a single line of output), which is the +# default; an ellipsis (`...` is suffixed, to indicate the truncation): # -# - Printed with truncation (to fit on a single line of output), -# which is the default; -# an ellipsis (<tt>...</tt> is suffixed, to indicate the truncation): +# irb(main):001> x = 'abc' * 100 # -# irb(main):001> x = 'abc' * 100 -# => "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc... # -# - Printed in full (regardless of the length). -# - Suppressed (not printed at all) +# > "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc... +# +# * Printed in full (regardless of the length). +# * Suppressed (not printed at all) +# # # You can change the initial behavior by: # -# - Adding to the configuration file: <tt>IRB.conf[:ECHO_ON_ASSIGNMENT] = false</tt>. -# (The default value for this entry is +niL+, which means the same as +:truncate+.) -# - Giving command-line option <tt>--noecho-on-assignment</tt> -# or <tt>--echo-on-assignment</tt>. -# (The default is <tt>--truncate-echo-on-assigment</tt>.) +# * Adding to the configuration file: `IRB.conf[:ECHO_ON_ASSIGNMENT] = false`. +# (The default value for this entry is `niL`, which means the same as +# `:truncate`.) +# * Giving command-line option `--noecho-on-assignment` or +# `--echo-on-assignment`. (The default is `--truncate-echo-on-assignment`.) +# # -# During the session, you can change the current setting -# with configuration method <tt>conf.echo_on_assignment=</tt> -# (set to +true+, +false+, or +:truncate+). +# During the session, you can change the current setting with configuration +# method `conf.echo_on_assignment=` (set to `true`, `false`, or `:truncate`). # -# By default, \IRB formats returned values by calling method +inspect+. +# By default, IRB formats returned values by calling method `inspect`. # # You can change the initial behavior by: # -# - Adding to the configuration file: <tt>IRB.conf[:INSPECT_MODE] = false</tt>. -# (The default value for this entry is +true+.) -# - Giving command-line option <tt>--noinspect</tt>. -# (The default is <tt>--inspect</tt>.) +# * Adding to the configuration file: `IRB.conf[:INSPECT_MODE] = false`. (The +# default value for this entry is `true`.) +# * Giving command-line option `--noinspect`. (The default is `--inspect`.) # -# During the session, you can change the setting using method <tt>conf.inspect_mode=</tt>. # -# === Multiline Output +# During the session, you can change the setting using method +# `conf.inspect_mode=`. # -# By default, \IRB prefixes a newline to a multiline response. +# ### Multiline Output # -# You can change the initial default value by adding to the configuation file: +# By default, IRB prefixes a newline to a multiline response. # -# IRB.conf[:NEWLINE_BEFORE_MULTILINE_OUTPUT] = false +# You can change the initial default value by adding to the configuration file: # -# During a session, you can retrieve or set the value using -# methods <tt>conf.newline_before_multiline_output?</tt> -# and <tt>conf.newline_before_multiline_output=</tt>. +# IRB.conf[:NEWLINE_BEFORE_MULTILINE_OUTPUT] = false +# +# During a session, you can retrieve or set the value using methods +# `conf.newline_before_multiline_output?` and +# `conf.newline_before_multiline_output=`. # # Examples: # -# irb(main):001> conf.inspect_mode = false -# => false -# irb(main):002> "foo\nbar" -# => -# foo -# bar -# irb(main):003> conf.newline_before_multiline_output = false -# => false -# irb(main):004> "foo\nbar" -# => foo -# bar +# irb(main):001> conf.inspect_mode = false +# => false +# irb(main):002> "foo\nbar" +# => +# foo +# bar +# irb(main):003> conf.newline_before_multiline_output = false +# => false +# irb(main):004> "foo\nbar" +# => foo +# bar +# +# ### Evaluation History # -# === Evaluation History +# By default, IRB saves no history of evaluations (returned values), and the +# related methods `conf.eval_history`, `_`, and `__` are undefined. # -# By default, \IRB saves no history of evaluations (returned values), -# and the related methods <tt>conf.eval_history</tt>, <tt>_</tt>, -# and <tt>__</tt> are undefined. +# You can turn on that history, and set the maximum number of evaluations to be +# stored: # -# You can turn on that history, and set the maximum number of evaluations to be stored: +# * In the configuration file: add `IRB.conf[:EVAL_HISTORY] = *n*`. (Examples +# below assume that we've added `IRB.conf[:EVAL_HISTORY] = 5`.) +# * In the session (at any time): `conf.eval_history = *n*`. # -# - In the configuration file: add <tt>IRB.conf[:EVAL_HISTORY] = _n_</tt>. -# (Examples below assume that we've added <tt>IRB.conf[:EVAL_HISTORY] = 5</tt>.) -# - In the session (at any time): <tt>conf.eval_history = _n_</tt>. # -# If +n+ is zero, all evaluation history is stored. +# If `n` is zero, all evaluation history is stored. # # Doing either of the above: # -# - Sets the maximum size of the evaluation history; -# defines method <tt>conf.eval_history</tt>, -# which returns the maximum size +n+ of the evaluation history: -# -# irb(main):001> conf.eval_history = 5 -# => 5 -# irb(main):002> conf.eval_history -# => 5 -# -# - Defines variable <tt>_</tt>, which contains the most recent evaluation, -# or +nil+ if none; same as method <tt>conf.last_value</tt>: -# -# irb(main):003> _ -# => 5 -# irb(main):004> :foo -# => :foo -# irb(main):005> :bar -# => :bar -# irb(main):006> _ -# => :bar -# irb(main):007> _ -# => :bar -# -# - Defines variable <tt>__</tt>: -# -# - <tt>__</tt> unadorned: contains all evaluation history: -# -# irb(main):008> :foo -# => :foo -# irb(main):009> :bar -# => :bar -# irb(main):010> :baz -# => :baz -# irb(main):011> :bat -# => :bat -# irb(main):012> :bam -# => :bam -# irb(main):013> __ -# => -# 9 :bar -# 10 :baz -# 11 :bat -# 12 :bam -# irb(main):014> __ -# => -# 10 :baz -# 11 :bat -# 12 :bam -# 13 ...self-history... -# -# Note that when the evaluation is multiline, it is displayed differently. -# -# - <tt>__[</tt>_m_<tt>]</tt>: -# -# - Positive _m_: contains the evaluation for the given line number, -# or +nil+ if that line number is not in the evaluation history: -# -# irb(main):015> __[12] -# => :bam -# irb(main):016> __[1] -# => nil -# -# - Negative _m_: contains the +mth+-from-end evaluation, -# or +nil+ if that evaluation is not in the evaluation history: -# -# irb(main):017> __[-3] -# => :bam -# irb(main):018> __[-13] -# => nil -# -# - Zero _m_: contains +nil+: -# -# irb(main):019> __[0] -# => nil -# -# === Prompt and Return Formats -# -# By default, \IRB uses the prompt and return value formats -# defined in its +:DEFAULT+ prompt mode. -# -# ==== The Default Prompt and Return Format +# * Sets the maximum size of the evaluation history; defines method +# `conf.eval_history`, which returns the maximum size `n` of the evaluation +# history: +# +# irb(main):001> conf.eval_history = 5 +# => 5 +# irb(main):002> conf.eval_history +# => 5 +# +# * Defines variable `_`, which contains the most recent evaluation, or `nil` +# if none; same as method `conf.last_value`: +# +# irb(main):003> _ +# => 5 +# irb(main):004> :foo +# => :foo +# irb(main):005> :bar +# => :bar +# irb(main):006> _ +# => :bar +# irb(main):007> _ +# => :bar +# +# * Defines variable `__`: +# +# * `__` unadorned: contains all evaluation history: +# +# irb(main):008> :foo +# => :foo +# irb(main):009> :bar +# => :bar +# irb(main):010> :baz +# => :baz +# irb(main):011> :bat +# => :bat +# irb(main):012> :bam +# => :bam +# irb(main):013> __ +# => +# 9 :bar +# 10 :baz +# 11 :bat +# 12 :bam +# irb(main):014> __ +# => +# 10 :baz +# 11 :bat +# 12 :bam +# 13 ...self-history... +# +# Note that when the evaluation is multiline, it is displayed +# differently. +# +# * `__[`*m*`]`: +# +# * Positive *m*: contains the evaluation for the given line number, +# or `nil` if that line number is not in the evaluation history: +# +# irb(main):015> __[12] +# => :bam +# irb(main):016> __[1] +# => nil +# +# * Negative *m*: contains the `mth`-from-end evaluation, or `nil` if +# that evaluation is not in the evaluation history: +# +# irb(main):017> __[-3] +# => :bam +# irb(main):018> __[-13] +# => nil +# +# * Zero *m*: contains `nil`: +# +# irb(main):019> __[0] +# => nil +# +# +# +# +# ### Prompt and Return Formats +# +# By default, IRB uses the prompt and return value formats defined in its +# `:DEFAULT` prompt mode. +# +# #### The Default Prompt and Return Format # # The default prompt and return values look like this: # -# irb(main):001> 1 + 1 -# => 2 -# irb(main):002> 2 + 2 -# => 4 +# irb(main):001> 1 + 1 +# => 2 +# irb(main):002> 2 + 2 +# => 4 # # The prompt includes: # -# - The name of the running program (<tt>irb</tt>); -# see {IRB Name}[rdoc-ref:IRB@IRB+Name]. -# - The name of the current session (<tt>main</tt>); -# See {IRB Sessions}[rdoc-ref:IRB@IRB+Sessions]. -# - A 3-digit line number (1-based). +# * The name of the running program (`irb`); see [IRB +# Name](rdoc-ref:IRB@IRB+Name). +# * The name of the current session (`main`); See [IRB +# Sessions](rdoc-ref:IRB@IRB+Sessions). +# * A 3-digit line number (1-based). +# # # The default prompt actually defines three formats: # -# - One for most situations (as above): +# * One for most situations (as above): +# +# irb(main):003> Dir +# => Dir # -# irb(main):003> Dir -# => Dir +# * One for when the typed command is a statement continuation (adds trailing +# asterisk): # -# - One for when the typed command is a statement continuation (adds trailing asterisk): +# irb(main):004* Dir. # -# irb(main):004* Dir. +# * One for when the typed command is a string continuation (adds trailing +# single-quote): # -# - One for when the typed command is a string continuation (adds trailing single-quote): +# irb(main):005' Dir.entries('. # -# irb(main):005' Dir.entries('. # # You can see the prompt change as you type the characters in the following: # @@ -570,276 +578,268 @@ require_relative "irb/pager" # irb(main):003> end # => ["README.md", "Rakefile"] # -# ==== Pre-Defined Prompts +# #### Pre-Defined Prompts # -# \IRB has several pre-defined prompts, stored in hash <tt>IRB.conf[:PROMPT]</tt>: +# IRB has several pre-defined prompts, stored in hash `IRB.conf[:PROMPT]`: # -# irb(main):001> IRB.conf[:PROMPT].keys -# => [:NULL, :DEFAULT, :CLASSIC, :SIMPLE, :INF_RUBY, :XMP] +# irb(main):001> IRB.conf[:PROMPT].keys +# => [:NULL, :DEFAULT, :CLASSIC, :SIMPLE, :INF_RUBY, :XMP] # -# To see the full data for these, type <tt>IRB.conf[:PROMPT]</tt>. +# To see the full data for these, type `IRB.conf[:PROMPT]`. # -# Most of these prompt definitions include specifiers that represent -# values like the \IRB name, session name, and line number; -# see {Prompt Specifiers}[rdoc-ref:IRB@Prompt+Specifiers]. +# Most of these prompt definitions include specifiers that represent values like +# the IRB name, session name, and line number; see [Prompt +# Specifiers](rdoc-ref:IRB@Prompt+Specifiers). # # You can change the initial prompt and return format by: # -# - Adding to the configuration file: <tt>IRB.conf[:PROMPT] = _mode_</tt> -# where _mode_ is the symbol name of a prompt mode. -# - Giving a command-line option: +# * Adding to the configuration file: `IRB.conf[:PROMPT] = *mode*` where +# *mode* is the symbol name of a prompt mode. +# * Giving a command-line option: +# +# * `--prompt *mode*`: sets the prompt mode to *mode*. where *mode* is the +# symbol name of a prompt mode. +# * `--simple-prompt` or `--sample-book-mode`: sets the prompt mode to +# `:SIMPLE`. +# * `--inf-ruby-mode`: sets the prompt mode to `:INF_RUBY` and suppresses +# both `--multiline` and `--singleline`. +# * `--noprompt`: suppresses prompting; does not affect echoing. +# # -# - <tt>--prompt _mode_</tt>: sets the prompt mode to _mode_. -# where _mode_ is the symbol name of a prompt mode. -# - <tt>--simple-prompt</tt> or <tt>--sample-book-mode</tt>: -# sets the prompt mode to +:SIMPLE+. -# - <tt>--inf-ruby-mode</tt>: sets the prompt mode to +:INF_RUBY+ -# and suppresses both <tt>--multiline</tt> and <tt>--singleline</tt>. -# - <tt>--noprompt</tt>: suppresses prompting; does not affect echoing. # # You can retrieve or set the current prompt mode with methods # -# <tt>conf.prompt_mode</tt> and <tt>conf.prompt_mode=</tt>. +# `conf.prompt_mode` and `conf.prompt_mode=`. # # If you're interested in prompts and return formats other than the defaults, # you might experiment by trying some of the others. # -# ==== Custom Prompts +# #### Custom Prompts +# +# You can also define custom prompts and return formats, which may be done +# either in an IRB session or in the configuration file. # -# You can also define custom prompts and return formats, -# which may be done either in an \IRB session or in the configuration file. +# A prompt in IRB actually defines three prompts, as seen above. For simple +# custom data, we'll make all three the same: # -# A prompt in \IRB actually defines three prompts, as seen above. -# For simple custom data, we'll make all three the same: +# irb(main):001* IRB.conf[:PROMPT][:MY_PROMPT] = { +# irb(main):002* PROMPT_I: ': ', +# irb(main):003* PROMPT_C: ': ', +# irb(main):004* PROMPT_S: ': ', +# irb(main):005* RETURN: '=> ' +# irb(main):006> } +# => {:PROMPT_I=>": ", :PROMPT_C=>": ", :PROMPT_S=>": ", :RETURN=>"=> "} # -# irb(main):001* IRB.conf[:PROMPT][:MY_PROMPT] = { -# irb(main):002* PROMPT_I: ': ', -# irb(main):003* PROMPT_C: ': ', -# irb(main):004* PROMPT_S: ': ', -# irb(main):005* RETURN: '=> ' -# irb(main):006> } -# => {:PROMPT_I=>": ", :PROMPT_C=>": ", :PROMPT_S=>": ", :RETURN=>"=> "} +# If you define the custom prompt in the configuration file, you can also make +# it the current prompt by adding: # -# If you define the custom prompt in the configuration file, -# you can also make it the current prompt by adding: +# IRB.conf[:PROMPT_MODE] = :MY_PROMPT # -# IRB.conf[:PROMPT_MODE] = :MY_PROMPT +# Regardless of where it's defined, you can make it the current prompt in a +# session: # -# Regardless of where it's defined, you can make it the current prompt in a session: +# conf.prompt_mode = :MY_PROMPT # -# conf.prompt_mode = :MY_PROMPT +# You can view or modify the current prompt data with various configuration +# methods: # -# You can view or modify the current prompt data with various configuration methods: +# * `conf.prompt_mode`, `conf.prompt_mode=`. +# * `conf.prompt_c`, `conf.c=`. +# * `conf.prompt_i`, `conf.i=`. +# * `conf.prompt_s`, `conf.s=`. +# * `conf.return_format`, `return_format=`. # -# - <tt>conf.prompt_mode</tt>, <tt>conf.prompt_mode=</tt>. -# - <tt>conf.prompt_c</tt>, <tt>conf.c=</tt>. -# - <tt>conf.prompt_i</tt>, <tt>conf.i=</tt>. -# - <tt>conf.prompt_s</tt>, <tt>conf.s=</tt>. -# - <tt>conf.return_format</tt>, <tt>return_format=</tt>. # -# ==== Prompt Specifiers +# #### Prompt Specifiers # -# A prompt's definition can include specifiers for which certain values are substituted: +# A prompt's definition can include specifiers for which certain values are +# substituted: # -# - <tt>%N</tt>: the name of the running program. -# - <tt>%m</tt>: the value of <tt>self.to_s</tt>. -# - <tt>%M</tt>: the value of <tt>self.inspect</tt>. -# - <tt>%l</tt>: an indication of the type of string; -# one of <tt>"</tt>, <tt>'</tt>, <tt>/</tt>, <tt>]</tt>. -# - <tt><i>NN</i>i</tt>: Indentation level. -# - <tt><i>NN</i>n</tt>: Line number. -# - <tt>%%</tt>: Literal <tt>%</tt>. +# * `%N`: the name of the running program. +# * `%m`: the value of `self.to_s`. +# * `%M`: the value of `self.inspect`. +# * `%l`: an indication of the type of string; one of `"`, `'`, `/`, `]`. +# * `%NNi`: Indentation level. NN is a 2-digit number that specifies the number +# of digits of the indentation level (03 will result in 001). +# * `%NNn`: Line number. NN is a 2-digit number that specifies the number +# of digits of the line number (03 will result in 001). +# * `%%`: Literal `%`. # -# === Verbosity # -# By default, \IRB verbosity is disabled, which means that output is smaller +# ### Verbosity +# +# By default, IRB verbosity is disabled, which means that output is smaller # rather than larger. # # You can enable verbosity by: # -# - Adding to the configuration file: <tt>IRB.conf[:VERBOSE] = true</tt> -# (the default is +nil+). -# - Giving command-line options <tt>--verbose</tt> -# (the default is <tt>--noverbose</tt>). +# * Adding to the configuration file: `IRB.conf[:VERBOSE] = true` (the default +# is `nil`). +# * Giving command-line options `--verbose` (the default is `--noverbose`). +# # # During a session, you can retrieve or set verbosity with methods -# <tt>conf.verbose</tt> and <tt>conf.verbose=</tt>. +# `conf.verbose` and `conf.verbose=`. +# +# ### Help # -# === Help +# Command-line option `--version` causes IRB to print its help text and exit. # -# Command-line option <tt>--version</tt> causes \IRB to print its help text -# and exit. +# ### Version # -# === Version +# Command-line option `--version` causes IRB to print its version text and exit. # -# Command-line option <tt>--version</tt> causes \IRB to print its version text -# and exit. +# ## Input and Output # -# == Input and Output +# ### Color Highlighting # -# === \Color Highlighting +# By default, IRB color highlighting is enabled, and is used for both: # -# By default, \IRB color highlighting is enabled, and is used for both: +# * Input: As you type, IRB reads the typed characters and highlights elements +# that it recognizes; it also highlights errors such as mismatched +# parentheses. +# * Output: IRB highlights syntactical elements. # -# - Input: As you type, \IRB reads the typed characters and highlights -# elements that it recognizes; -# it also highlights errors such as mismatched parentheses. -# - Output: \IRB highlights syntactical elements. # # You can disable color highlighting by: # -# - Adding to the configuration file: <tt>IRB.conf[:USE_COLORIZE] = false</tt> -# (the default value is +true+). -# - Giving command-line option <tt>--nocolorize</tt> +# * Adding to the configuration file: `IRB.conf[:USE_COLORIZE] = false` (the +# default value is `true`). +# * Giving command-line option `--nocolorize` # -# == Debugging # -# Command-line option <tt>-d</tt> sets variables <tt>$VERBOSE</tt> -# and <tt>$DEBUG</tt> to +true+; -# these have no effect on \IRB output. +# ## Debugging # -# === Warnings +# Command-line option `-d` sets variables `$VERBOSE` and `$DEBUG` to `true`; +# these have no effect on IRB output. # -# Command-line option <tt>-w</tt> suppresses warnings. +# ### Warnings # -# Command-line option <tt>-W[_level_]<tt> -# sets warning level; 0=silence, 1=medium, 2=verbose. +# Command-line option `-w` suppresses warnings. # -# :stopdoc: -# === Performance Measurement +# Command-line option `-W[*level*]` sets warning level; # -# IRB.conf[:MEASURE] IRB.conf[:MEASURE_CALLBACKS] IRB.conf[:MEASURE_PROC] -# :startdoc: +# * 0=silence +# * 1=medium +# * 2=verbose # -# == Other Features +# ## Other Features # -# === Load Modules +# ### Load Modules # # You can specify the names of modules that are to be required at startup. # -# \Array <tt>conf.load_modules</tt> determines the modules (if any) -# that are to be required during session startup. -# The array is used only during session startup, -# so the initial value is the only one that counts. +# Array `conf.load_modules` determines the modules (if any) that are to be +# required during session startup. The array is used only during session +# startup, so the initial value is the only one that counts. # -# The default initial value is <tt>[]</tt> (load no modules): +# The default initial value is `[]` (load no modules): # -# irb(main):001> conf.load_modules -# => [] +# irb(main):001> conf.load_modules +# => [] # # You can set the default initial value via: # -# - Command-line option <tt>-r</tt> +# * Command-line option `-r` +# +# $ irb -r csv -r json +# irb(main):001> conf.load_modules +# => ["csv", "json"] # -# $ irb -r csv -r json -# irb(main):001> conf.load_modules -# => ["csv", "json"] +# * Hash entry `IRB.conf[:LOAD_MODULES] = *array*`: # -# - \Hash entry <tt>IRB.conf[:LOAD_MODULES] = _array_</tt>: +# IRB.conf[:LOAD_MODULES] = %w[csv, json] # -# IRB.conf[:LOAD_MODULES] = %w[csv, json] # # Note that the configuration file entry overrides the command-line options. # -# === RI Documentation Directories +# ### RI Documentation Directories # -# You can specify the paths to RI documentation directories -# that are to be loaded (in addition to the default directories) at startup; -# see details about RI by typing <tt>ri --help</tt>. +# You can specify the paths to RI documentation directories that are to be +# loaded (in addition to the default directories) at startup; see details about +# RI by typing `ri --help`. # -# \Array <tt>conf.extra_doc_dirs</tt> determines the directories (if any) -# that are to be loaded during session startup. -# The array is used only during session startup, +# Array `conf.extra_doc_dirs` determines the directories (if any) that are to be +# loaded during session startup. The array is used only during session startup, # so the initial value is the only one that counts. # -# The default initial value is <tt>[]</tt> (load no extra documentation): -# -# irb(main):001> conf.extra_doc_dirs -# => [] +# The default initial value is `[]` (load no extra documentation): # -# You can set the default initial value via: -# -# - Command-line option <tt>--extra_doc_dir</tt> -# -# $ irb --extra-doc-dir your_doc_dir --extra-doc-dir my_doc_dir # irb(main):001> conf.extra_doc_dirs -# => ["your_doc_dir", "my_doc_dir"] +# => [] # -# - \Hash entry <tt>IRB.conf[:EXTRA_DOC_DIRS] = _array_</tt>: +# You can set the default initial value via: # -# IRB.conf[:EXTRA_DOC_DIRS] = %w[your_doc_dir my_doc_dir] +# * Command-line option `--extra_doc_dir` # -# Note that the configuration file entry overrides the command-line options. +# $ irb --extra-doc-dir your_doc_dir --extra-doc-dir my_doc_dir +# irb(main):001> conf.extra_doc_dirs +# => ["your_doc_dir", "my_doc_dir"] # -# :stopdoc: -# === \Context Mode +# * Hash entry `IRB.conf[:EXTRA_DOC_DIRS] = *array*`: # -# IRB.conf[:CONTEXT_MODE] -# :startdoc: +# IRB.conf[:EXTRA_DOC_DIRS] = %w[your_doc_dir my_doc_dir] # -# === \IRB Name # -# You can specify a name for \IRB. +# Note that the configuration file entry overrides the command-line options. # -# The default initial value is <tt>'irb'</tt>: +# ### IRB Name # -# irb(main):001> conf.irb_name -# => "irb" +# You can specify a name for IRB. # -# You can set the default initial value -# via hash entry <tt>IRB.conf[:IRB_NAME] = _string_</tt>: +# The default initial value is `'irb'`: # -# IRB.conf[:IRB_NAME] = 'foo' +# irb(main):001> conf.irb_name +# => "irb" # -# === Application Name +# You can set the default initial value via hash entry `IRB.conf[:IRB_NAME] = +# *string*`: # -# You can specify an application name for the \IRB session. +# IRB.conf[:IRB_NAME] = 'foo' # -# The default initial value is <tt>'irb'</tt>: +# ### Application Name # -# irb(main):001> conf.ap_name -# => "irb" +# You can specify an application name for the IRB session. # -# You can set the default initial value -# via hash entry <tt>IRB.conf[:AP_NAME] = _string_</tt>: +# The default initial value is `'irb'`: # -# IRB.conf[:AP_NAME] = 'my_ap_name' +# irb(main):001> conf.ap_name +# => "irb" # -# === Configuration Monitor +# You can set the default initial value via hash entry `IRB.conf[:AP_NAME] = +# *string*`: # -# You can monitor changes to the configuration by assigning a proc -# to <tt>IRB.conf[:IRB_RC]</tt> in the configuration file: +# IRB.conf[:AP_NAME] = 'my_ap_name' # -# IRB.conf[:IRB_RC] = proc {|conf| puts conf.class } +# ### Configuration Monitor # -# Each time the configuration is changed, -# that proc is called with argument +conf+: +# You can monitor changes to the configuration by assigning a proc to +# `IRB.conf[:IRB_RC]` in the configuration file: # -# :stopdoc: -# === \Locale +# IRB.conf[:IRB_RC] = proc {|conf| puts conf.class } # -# IRB.conf[:LC_MESSAGES] -# :startdoc: +# Each time the configuration is changed, that proc is called with argument +# `conf`: # -# === Encodings +# ### Encodings # -# Command-line option <tt>-E _ex_[:_in_]</tt> -# sets initial external (ex) and internal (in) encodings. +# Command-line option `-E *ex*[:*in*]` sets initial external (ex) and internal +# (in) encodings. # -# Command-line option <tt>-U</tt> sets both to UTF-8. +# Command-line option `-U` sets both to UTF-8. # -# === Commands +# ### Commands # -# Please use the `show_cmds` command to see the list of available commands. +# Please use the `help` command to see the list of available commands. # -# === IRB Sessions +# ### IRB Sessions # # IRB has a special feature, that allows you to manage many sessions at once. # # You can create new sessions with Irb.irb, and get a list of current sessions -# with the +jobs+ command in the prompt. +# with the `jobs` command in the prompt. # -# ==== Configuration +# #### Configuration # # The command line options, or IRB.conf, specify the default behavior of # Irb.irb. @@ -847,32 +847,33 @@ require_relative "irb/pager" # On the other hand, each conf in IRB@Command-Line+Options is used to # individually configure IRB.irb. # -# If a proc is set for <code>IRB.conf[:IRB_RC]</code>, its will be invoked after execution +# If a proc is set for `IRB.conf[:IRB_RC]`, its will be invoked after execution # of that proc with the context of the current session as its argument. Each # session can be configured using this mechanism. # -# ==== Session variables +# #### Session variables # # There are a few variables in every Irb session that can come in handy: # -# <code>_</code>:: -# The value command executed, as a local variable -# <code>__</code>:: -# The history of evaluated commands. Available only if -# <code>IRB.conf[:EVAL_HISTORY]</code> is not +nil+ (which is the default). -# See also IRB::Context#eval_history= and IRB::History. -# <code>__[line_no]</code>:: -# Returns the evaluation value at the given line number, +line_no+. -# If +line_no+ is a negative, the return value +line_no+ many lines before -# the most recent return value. +# `_` +# : The value command executed, as a local variable +# `__` +# : The history of evaluated commands. Available only if +# `IRB.conf[:EVAL_HISTORY]` is not `nil` (which is the default). See also +# IRB::Context#eval_history= and IRB::History. +# `__[line_no]` +# : Returns the evaluation value at the given line number, `line_no`. If +# `line_no` is a negative, the return value `line_no` many lines before the +# most recent return value. # -# == Restrictions # -# Ruby code typed into \IRB behaves the same as Ruby code in a file, except that: +# ## Restrictions # -# - Because \IRB evaluates input immediately after it is syntactically complete, -# some results may be slightly different. -# - Forking may not be well behaved. +# Ruby code typed into IRB behaves the same as Ruby code in a file, except that: +# +# * Because IRB evaluates input immediately after it is syntactically +# complete, some results may be slightly different. +# * Forking may not be well behaved. # module IRB @@ -881,14 +882,14 @@ module IRB # The current IRB::Context of the session, see IRB.conf # - # irb - # irb(main):001:0> IRB.CurrentContext.irb_name = "foo" - # foo(main):002:0> IRB.conf[:MAIN_CONTEXT].irb_name #=> "foo" - def IRB.CurrentContext + # irb + # irb(main):001:0> IRB.CurrentContext.irb_name = "foo" + # foo(main):002:0> IRB.conf[:MAIN_CONTEXT].irb_name #=> "foo" + def IRB.CurrentContext # :nodoc: IRB.conf[:MAIN_CONTEXT] end - # Initializes IRB and creates a new Irb.irb object at the +TOPLEVEL_BINDING+ + # Initializes IRB and creates a new Irb.irb object at the `TOPLEVEL_BINDING` def IRB.start(ap_path = nil) STDOUT.sync = true $0 = File::basename(ap_path, ".rb") if ap_path @@ -904,42 +905,46 @@ module IRB end # Quits irb - def IRB.irb_exit(irb, ret) - throw :IRB_EXIT, ret + def IRB.irb_exit(*) # :nodoc: + throw :IRB_EXIT, false end # Aborts then interrupts irb. # - # Will raise an Abort exception, or the given +exception+. - def IRB.irb_abort(irb, exception = Abort) + # Will raise an Abort exception, or the given `exception`. + def IRB.irb_abort(irb, exception = Abort) # :nodoc: irb.context.thread.raise exception, "abort then interrupt!" end class Irb # Note: instance and index assignment expressions could also be written like: - # "foo.bar=(1)" and "foo.[]=(1, bar)", when expressed that way, the former - # be parsed as :assign and echo will be suppressed, but the latter is - # parsed as a :method_add_arg and the output won't be suppressed + # "foo.bar=(1)" and "foo.[]=(1, bar)", when expressed that way, the former be + # parsed as :assign and echo will be suppressed, but the latter is parsed as a + # :method_add_arg and the output won't be suppressed PROMPT_MAIN_TRUNCATE_LENGTH = 32 - PROMPT_MAIN_TRUNCATE_OMISSION = '...'.freeze - CONTROL_CHARACTERS_PATTERN = "\x00-\x1F".freeze + PROMPT_MAIN_TRUNCATE_OMISSION = '...' + CONTROL_CHARACTERS_PATTERN = "\x00-\x1F" # Returns the current context of this irb session attr_reader :context # The lexer used by this irb session attr_accessor :scanner + attr_reader :from_binding + # Creates a new irb session - def initialize(workspace = nil, input_method = nil) + def initialize(workspace = nil, input_method = nil, from_binding: false) + @from_binding = from_binding @context = Context.new(self, workspace, input_method) - @context.workspace.load_commands_to_main + @context.workspace.load_helper_methods_to_main @signal_status = :IN_IRB @scanner = RubyLex.new @line_no = 1 end - # A hook point for `debug` command's breakpoint after :IRB_EXIT as well as its clean-up + # A hook point for `debug` command's breakpoint after :IRB_EXIT as well as its + # clean-up def debug_break # it means the debug integration has been activated if defined?(DEBUGGER__) && DEBUGGER__.respond_to?(:capture_frames_without_irb) @@ -952,29 +957,37 @@ module IRB def debug_readline(binding) workspace = IRB::WorkSpace.new(binding) - context.workspace = workspace - context.workspace.load_commands_to_main + context.replace_workspace(workspace) + context.workspace.load_helper_methods_to_main @line_no += 1 # When users run: - # 1. Debugging commands, like `step 2` - # 2. Any input that's not irb-command, like `foo = 123` + # 1. Debugging commands, like `step 2` + # 2. Any input that's not irb-command, like `foo = 123` + # # - # Irb#eval_input will simply return the input, and we need to pass it to the debugger. - input = if IRB.conf[:SAVE_HISTORY] && context.io.support_history_saving? - # Previous IRB session's history has been saved when `Irb#run` is exited - # We need to make sure the saved history is not saved again by reseting the counter - context.io.reset_history_counter + # Irb#eval_input will simply return the input, and we need to pass it to the + # debugger. + input = nil + forced_exit = catch(:IRB_EXIT) do + if IRB.conf[:SAVE_HISTORY] && context.io.support_history_saving? + # Previous IRB session's history has been saved when `Irb#run` is exited We need + # to make sure the saved history is not saved again by resetting the counter + context.io.reset_history_counter - begin - eval_input - ensure - context.io.save_history + begin + input = eval_input + ensure + context.io.save_history + end + else + input = eval_input end - else - eval_input + false end + Kernel.exit if forced_exit + if input&.include?("\n") @line_no += input.count("\n") - 1 end @@ -985,6 +998,7 @@ module IRB def run(conf = IRB.conf) in_nested_session = !!conf[:MAIN_CONTEXT] conf[:IRB_RC].call(context) if conf[:IRB_RC] + prev_context = conf[:MAIN_CONTEXT] conf[:MAIN_CONTEXT] = context save_history = !in_nested_session && conf[:SAVE_HISTORY] && context.io.support_history_saving? @@ -998,13 +1012,24 @@ module IRB end begin - catch(:IRB_EXIT) do + if defined?(RubyVM.keep_script_lines) + keep_script_lines_backup = RubyVM.keep_script_lines + RubyVM.keep_script_lines = true + end + + forced_exit = catch(:IRB_EXIT) do eval_input end ensure + # Do not restore to nil. It will cause IRB crash when used with threads. + IRB.conf[:MAIN_CONTEXT] = prev_context if prev_context + + RubyVM.keep_script_lines = keep_script_lines_backup if defined?(RubyVM.keep_script_lines) trap("SIGINT", prev_trap) conf[:AT_EXIT].each{|hook| hook.call} + context.io.save_history if save_history + Kernel.exit if forced_exit end end @@ -1015,12 +1040,13 @@ module IRB each_top_level_statement do |statement, line_no| signal_status(:IN_EVAL) do begin - # If the integration with debugger is activated, we return certain input if it should be dealt with by debugger + # If the integration with debugger is activated, we return certain input if it + # should be dealt with by debugger if @context.with_debugger && statement.should_be_handled_by_debugger? return statement.code end - @context.evaluate(statement.evaluable_code, line_no) + @context.evaluate(statement, line_no) if @context.echo? && !statement.suppresses_echo? if statement.is_assignment? @@ -1067,7 +1093,7 @@ module IRB return read_input(prompt) if @context.io.respond_to?(:check_termination) # nomultiline - code = '' + code = +'' line_offset = 0 loop do line = read_input(prompt) @@ -1076,9 +1102,7 @@ module IRB end code << line - - # Accept any single-line input for symbol aliases or commands that transform args - return code if single_line_command?(code) + return code if command?(code) tokens, opens, terminated = @scanner.check_code_state(code, local_variables: @context.local_variables) return code if terminated @@ -1093,34 +1117,48 @@ module IRB loop do code = readmultiline break unless code - - if code != "\n" - yield build_statement(code), @line_no - end + yield build_statement(code), @line_no @line_no += code.count("\n") rescue RubyLex::TerminateLineInput end end def build_statement(code) + if code.match?(/\A\n*\z/) + return Statement::EmptyInput.new + end + code.force_encoding(@context.io.encoding) - command_or_alias, arg = code.split(/\s/, 2) - # Transform a non-identifier alias (@, $) or keywords (next, break) - command_name = @context.command_aliases[command_or_alias.to_sym] - command = command_name || command_or_alias - command_class = ExtendCommandBundle.load_command(command) - - if command_class - Statement::Command.new(code, command, arg, command_class) + if (command, arg = parse_command(code)) + command_class = Command.load_command(command) + Statement::Command.new(code, command_class, arg) else is_assignment_expression = @scanner.assignment_expression?(code, local_variables: @context.local_variables) Statement::Expression.new(code, is_assignment_expression) end end - def single_line_command?(code) - command = code.split(/\s/, 2).first - @context.symbol_alias?(command) || @context.transform_args?(command) + def parse_command(code) + command_name, arg = code.strip.split(/\s+/, 2) + return unless code.lines.size == 1 && command_name + + arg ||= '' + command = command_name.to_sym + # Command aliases are always command. example: $, @ + if (alias_name = @context.command_aliases[command]) + return [alias_name, arg] + end + + # Check visibility + public_method = !!Kernel.instance_method(:public_method).bind_call(@context.main, command) rescue false + private_method = !public_method && !!Kernel.instance_method(:method).bind_call(@context.main, command) rescue false + if Command.execute_as_command?(command, public_method: public_method, private_method: private_method) + [command, arg] + end + end + + def command?(code) + !!parse_command(code) end def configure_io @@ -1138,8 +1176,7 @@ module IRB false end else - # Accept any single-line input for symbol aliases or commands that transform args - next true if single_line_command?(code) + next true if command?(code) _tokens, _opens, terminated = @scanner.check_code_state(code, local_variables: @context.local_variables) terminated @@ -1148,13 +1185,13 @@ module IRB end if @context.io.respond_to?(:dynamic_prompt) @context.io.dynamic_prompt do |lines| - lines << '' if lines.empty? tokens = RubyLex.ripper_lex_without_warning(lines.map{ |l| l + "\n" }.join, local_variables: @context.local_variables) line_results = IRB::NestingParser.parse_by_line(tokens) tokens_until_line = [] line_results.map.with_index do |(line_tokens, _prev_opens, next_opens, _min_depth), line_num_offset| line_tokens.each do |token, _s| - # Avoid appending duplicated token. Tokens that include "\n" like multiline tstring_content can exist in multiple lines. + # Avoid appending duplicated token. Tokens that include "n" like multiline + # tstring_content can exist in multiple lines. tokens_until_line << token if token != tokens_until_line.last end continue = @scanner.should_continue?(tokens_until_line) @@ -1211,20 +1248,33 @@ module IRB irb_bug = true else irb_bug = false + # To support backtrace filtering while utilizing Exception#full_message, we need to clone + # the exception to avoid modifying the original exception's backtrace. + exc = exc.clone + filtered_backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact + backtrace_filter = IRB.conf[:BACKTRACE_FILTER] + + if backtrace_filter + if backtrace_filter.respond_to?(:call) + filtered_backtrace = backtrace_filter.call(filtered_backtrace) + else + warn "IRB.conf[:BACKTRACE_FILTER] #{backtrace_filter} should respond to `call` method" + end + end + + exc.set_backtrace(filtered_backtrace) end - if RUBY_VERSION < '3.0.0' - if STDOUT.tty? - message = exc.full_message(order: :bottom) - order = :bottom - else - message = exc.full_message(order: :top) - order = :top + highlight = Color.colorable? + + order = + if RUBY_VERSION < '3.0.0' + STDOUT.tty? ? :bottom : :top + else # '3.0.0' <= RUBY_VERSION + :top end - else # '3.0.0' <= RUBY_VERSION - message = exc.full_message(order: :top) - order = :top - end + + message = exc.full_message(order: order, highlight: highlight) message = convert_invalid_byte_sequence(message, exc.message.encoding) message = encode_with_invalid_byte_sequence(message, IRB.conf[:LC_MESSAGES].encoding) unless message.encoding.to_s.casecmp?(IRB.conf[:LC_MESSAGES].encoding.to_s) message = message.gsub(/((?:^\t.+$\n)+)/) { |m| @@ -1235,7 +1285,6 @@ module IRB lines = m.split("\n").reverse end unless irb_bug - lines = lines.map { |l| @context.workspace.filter_backtrace(l) }.compact if lines.size > @context.back_trace_limit omit = lines.size - @context.back_trace_limit lines = lines[0..(@context.back_trace_limit - 1)] @@ -1246,7 +1295,7 @@ module IRB lines.map{ |l| l + "\n" }.join } # The "<top (required)>" in "(irb)" may be the top level of IRB so imitate the main object. - message = message.gsub(/\(irb\):(?<num>\d+):in `<(?<frame>top \(required\))>'/) { "(irb):#{$~[:num]}:in `<main>'" } + message = message.gsub(/\(irb\):(?<num>\d+):in (?<open_quote>[`'])<(?<frame>top \(required\))>'/) { "(irb):#{$~[:num]}:in #{$~[:open_quote]}<main>'" } puts message puts 'Maybe IRB bug!' if irb_bug rescue Exception => handler_exc @@ -1258,11 +1307,10 @@ module IRB end end - # Evaluates the given block using the given +path+ as the Context#irb_path - # and +name+ as the Context#irb_name. + # Evaluates the given block using the given `path` as the Context#irb_path and + # `name` as the Context#irb_name. # - # Used by the irb command +source+, see IRB@IRB+Sessions for more - # information. + # Used by the irb command `source`, see IRB@IRB+Sessions for more information. def suspend_name(path = nil, name = nil) @context.irb_path, back_path = path, @context.irb_path if path @context.irb_name, back_name = name, @context.irb_name if name @@ -1274,25 +1322,22 @@ module IRB end end - # Evaluates the given block using the given +workspace+ as the + # Evaluates the given block using the given `workspace` as the # Context#workspace. # - # Used by the irb command +irb_load+, see IRB@IRB+Sessions for more - # information. + # Used by the irb command `irb_load`, see IRB@IRB+Sessions for more information. def suspend_workspace(workspace) - @context.workspace, back_workspace = workspace, @context.workspace - begin - yield back_workspace - ensure - @context.workspace = back_workspace - end + current_workspace = @context.workspace + @context.replace_workspace(workspace) + yield + ensure + @context.replace_workspace current_workspace end - # Evaluates the given block using the given +input_method+ as the - # Context#io. + # Evaluates the given block using the given `input_method` as the Context#io. # - # Used by the irb commands +source+ and +irb_load+, see IRB@IRB+Sessions - # for more information. + # Used by the irb commands `source` and `irb_load`, see IRB@IRB+Sessions for + # more information. def suspend_input_method(input_method) back_io = @context.io @context.instance_eval{@io = input_method} @@ -1325,7 +1370,7 @@ module IRB end end - # Evaluates the given block using the given +status+. + # Evaluates the given block using the given `status`. def signal_status(status) return yield if @signal_status == :IN_LOAD @@ -1375,8 +1420,8 @@ module IRB Pager.page_content(format(@context.return_format, str), retain_content: true) end - # Outputs the local variables to this current session, including - # #signal_status and #context, using IRB::Locale. + # Outputs the local variables to this current session, including #signal_status + # and #context, using IRB::Locale. def inspect ary = [] for iv in instance_variables @@ -1434,7 +1479,7 @@ module IRB end def format_prompt(format, ltype, indent, line_no) # :nodoc: - format.gsub(/%([0-9]+)?([a-zA-Z])/) do + format.gsub(/%([0-9]+)?([a-zA-Z%])/) do case $2 when "N" @context.irb_name @@ -1467,7 +1512,7 @@ module IRB line_no.to_s end when "%" - "%" + "%" unless $1 end end end @@ -1475,12 +1520,11 @@ module IRB end class Binding - # Opens an IRB session where +binding.irb+ is called which allows for - # interactive debugging. You can call any methods or variables available in - # the current scope, and mutate state if you need to. - # + # Opens an IRB session where `binding.irb` is called which allows for + # interactive debugging. You can call any methods or variables available in the + # current scope, and mutate state if you need to. # - # Given a Ruby file called +potato.rb+ containing the following code: + # Given a Ruby file called `potato.rb` containing the following code: # # class Potato # def initialize @@ -1492,8 +1536,8 @@ class Binding # # Potato.new # - # Running <code>ruby potato.rb</code> will open an IRB session where - # +binding.irb+ is called, and you will see the following: + # Running `ruby potato.rb` will open an IRB session where `binding.irb` is + # called, and you will see the following: # # $ ruby potato.rb # @@ -1523,8 +1567,8 @@ class Binding # irb(#<Potato:0x00007feea1916670>):004:0> @cooked = true # => true # - # You can exit the IRB session with the +exit+ command. Note that exiting will - # resume execution where +binding.irb+ had paused it, as you can see from the + # You can exit the IRB session with the `exit` command. Note that exiting will + # resume execution where `binding.irb` had paused it, as you can see from the # output printed to standard output in this example: # # irb(#<Potato:0x00007feea1916670>):005:0> exit @@ -1533,7 +1577,7 @@ class Binding # See IRB for more information. def irb(show_code: true) # Setup IRB with the current file's path and no command line arguments - IRB.setup(source_location[0], argv: []) + IRB.setup(source_location[0], argv: []) unless IRB.initialized? # Create a new workspace using the current binding workspace = IRB::WorkSpace.new(self) # Print the code around the binding if show_code is true @@ -1545,16 +1589,17 @@ class Binding if debugger_irb # If we're already in a debugger session, set the workspace and irb_path for the original IRB instance - debugger_irb.context.workspace = workspace + debugger_irb.context.replace_workspace(workspace) debugger_irb.context.irb_path = irb_path - # If we've started a debugger session and hit another binding.irb, we don't want to start an IRB session - # instead, we want to resume the irb:rdbg session. + # If we've started a debugger session and hit another binding.irb, we don't want + # to start an IRB session instead, we want to resume the irb:rdbg session. IRB::Debug.setup(debugger_irb) IRB::Debug.insert_debug_break debugger_irb.debug_break else - # If we're not in a debugger session, create a new IRB instance with the current workspace - binding_irb = IRB::Irb.new(workspace) + # If we're not in a debugger session, create a new IRB instance with the current + # workspace + binding_irb = IRB::Irb.new(workspace, from_binding: true) binding_irb.context.irb_path = irb_path binding_irb.run(IRB.conf) binding_irb.debug_break diff --git a/lib/irb/cmd/backtrace.rb b/lib/irb/cmd/backtrace.rb deleted file mode 100644 index f632894618..0000000000 --- a/lib/irb/cmd/backtrace.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Backtrace < DebugCommand - def self.transform_args(args) - args&.dump - end - - def execute(*args) - super(pre_cmds: ["backtrace", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/break.rb b/lib/irb/cmd/break.rb deleted file mode 100644 index df259a90ca..0000000000 --- a/lib/irb/cmd/break.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Break < DebugCommand - def self.transform_args(args) - args&.dump - end - - def execute(args = nil) - super(pre_cmds: "break #{args}") - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/catch.rb b/lib/irb/cmd/catch.rb deleted file mode 100644 index 40b62c7533..0000000000 --- a/lib/irb/cmd/catch.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Catch < DebugCommand - def self.transform_args(args) - args&.dump - end - - def execute(*args) - super(pre_cmds: ["catch", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/chws.rb b/lib/irb/cmd/chws.rb deleted file mode 100644 index 31045f9bbb..0000000000 --- a/lib/irb/cmd/chws.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: false -# -# change-ws.rb - -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# - -require_relative "nop" -require_relative "../ext/change-ws" - -module IRB - # :stopdoc: - - module ExtendCommand - - class CurrentWorkingWorkspace < Nop - category "Workspace" - description "Show the current workspace." - - def execute(*obj) - irb_context.main - end - end - - class ChangeWorkspace < Nop - category "Workspace" - description "Change the current workspace to an object." - - def execute(*obj) - irb_context.change_workspace(*obj) - irb_context.main - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/edit.rb b/lib/irb/cmd/edit.rb deleted file mode 100644 index 69606beea0..0000000000 --- a/lib/irb/cmd/edit.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'shellwords' -require_relative "nop" -require_relative "../source_finder" - -module IRB - # :stopdoc: - - module ExtendCommand - class Edit < Nop - category "Misc" - description 'Open a file with the editor command defined with `ENV["VISUAL"]` or `ENV["EDITOR"]`.' - - class << self - def transform_args(args) - # Return a string literal as is for backward compatibility - if args.nil? || args.empty? || string_literal?(args) - args - else # Otherwise, consider the input as a String for convenience - args.strip.dump - end - end - end - - def execute(*args) - path = args.first - - if path.nil? && (irb_path = @irb_context.irb_path) - path = irb_path - end - - if !File.exist?(path) - source = - begin - SourceFinder.new(@irb_context).find_source(path) - rescue NameError - # if user enters a path that doesn't exist, it'll cause NameError when passed here because find_source would try to evaluate it as well - # in this case, we should just ignore the error - end - - if source - path = source.file - else - puts "Can not find file: #{path}" - return - end - end - - if editor = (ENV['VISUAL'] || ENV['EDITOR']) - puts "command: '#{editor}'" - puts " path: #{path}" - system(*Shellwords.split(editor), path) - else - puts "Can not find editor setting: ENV['VISUAL'] or ENV['EDITOR']" - end - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/help.rb b/lib/irb/cmd/help.rb deleted file mode 100644 index 64b885c383..0000000000 --- a/lib/irb/cmd/help.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require_relative "show_doc" - -module IRB - module ExtendCommand - class Help < ShowDoc - category "Context" - description "[DEPRECATED] Enter the mode to look up RI documents." - - DEPRECATION_MESSAGE = <<~MSG - [Deprecation] The `help` command will be repurposed to display command help in the future. - For RI document lookup, please use the `show_doc` command instead. - For command help, please use `show_cmds` for now. - MSG - - def execute(*names) - warn DEPRECATION_MESSAGE - super - end - end - end -end diff --git a/lib/irb/cmd/info.rb b/lib/irb/cmd/info.rb deleted file mode 100644 index 2c0a32b34f..0000000000 --- a/lib/irb/cmd/info.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative "debug" - -module IRB - # :stopdoc: - - module ExtendCommand - class Info < DebugCommand - def self.transform_args(args) - args&.dump - end - - def execute(*args) - super(pre_cmds: ["info", *args].join(" ")) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/nop.rb b/lib/irb/cmd/nop.rb index 7fb197c51f..9d2e3c4d47 100644 --- a/lib/irb/cmd/nop.rb +++ b/lib/irb/cmd/nop.rb @@ -1,53 +1,4 @@ -# frozen_string_literal: false -# -# nop.rb - -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# +# frozen_string_literal: true -module IRB - # :stopdoc: - - module ExtendCommand - class CommandArgumentError < StandardError; end - - class Nop - class << self - def category(category = nil) - @category = category if category - @category - end - - def description(description = nil) - @description = description if description - @description - end - - private - - def string_literal?(args) - sexp = Ripper.sexp(args) - sexp && sexp.size == 2 && sexp.last&.first&.first == :string_literal - end - end - - def self.execute(irb_context, *opts, **kwargs, &block) - command = new(irb_context) - command.execute(*opts, **kwargs, &block) - rescue CommandArgumentError => e - puts e.message - end - - def initialize(irb_context) - @irb_context = irb_context - end - - attr_reader :irb_context - - def execute(*opts) - #nop - end - end - end - - # :startdoc: -end +# This file is just a placeholder for backward-compatibility. +# Please require 'irb' and inherit your command from `IRB::Command::Base` instead. diff --git a/lib/irb/cmd/pushws.rb b/lib/irb/cmd/pushws.rb deleted file mode 100644 index 59996ceb0c..0000000000 --- a/lib/irb/cmd/pushws.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: false -# -# change-ws.rb - -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# - -require_relative "nop" -require_relative "../ext/workspaces" - -module IRB - # :stopdoc: - - module ExtendCommand - class Workspaces < Nop - category "Workspace" - description "Show workspaces." - - def execute(*obj) - irb_context.workspaces.collect{|ws| ws.main} - end - end - - class PushWorkspace < Workspaces - category "Workspace" - description "Push an object to the workspace stack." - - def execute(*obj) - irb_context.push_workspace(*obj) - super - end - end - - class PopWorkspace < Workspaces - category "Workspace" - description "Pop a workspace from the workspace stack." - - def execute(*obj) - irb_context.pop_workspace(*obj) - super - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/show_cmds.rb b/lib/irb/cmd/show_cmds.rb deleted file mode 100644 index a8d899e4ac..0000000000 --- a/lib/irb/cmd/show_cmds.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require "stringio" -require_relative "nop" -require_relative "../pager" - -module IRB - # :stopdoc: - - module ExtendCommand - class ShowCmds < Nop - category "IRB" - description "List all available commands and their description." - - def execute(*args) - commands_info = IRB::ExtendCommandBundle.all_commands_info - commands_grouped_by_categories = commands_info.group_by { |cmd| cmd[:category] } - - user_aliases = irb_context.instance_variable_get(:@user_aliases) - - commands_grouped_by_categories["Aliases"] = user_aliases.map do |alias_name, target| - { display_name: alias_name, description: "Alias for `#{target}`" } - end - - if irb_context.with_debugger - # Remove the original "Debugging" category - commands_grouped_by_categories.delete("Debugging") - # Remove the `help` command as it's delegated to the debugger - commands_grouped_by_categories["Context"].delete_if { |cmd| cmd[:display_name] == :help } - # Add an empty "Debugging (from debug.gem)" category at the end - commands_grouped_by_categories["Debugging (from debug.gem)"] = [] - end - - longest_cmd_name_length = commands_info.map { |c| c[:display_name].length }.max - - output = StringIO.new - - commands_grouped_by_categories.each do |category, cmds| - output.puts Color.colorize(category, [:BOLD]) - - cmds.each do |cmd| - output.puts " #{cmd[:display_name].to_s.ljust(longest_cmd_name_length)} #{cmd[:description]}" - end - - output.puts - end - - # Append the debugger help at the end - if irb_context.with_debugger - output.puts DEBUGGER__.help - end - - Pager.page_content(output.string) - end - end - end - - # :startdoc: -end diff --git a/lib/irb/cmd/show_doc.rb b/lib/irb/cmd/show_doc.rb deleted file mode 100644 index 99dd9ab95a..0000000000 --- a/lib/irb/cmd/show_doc.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require_relative "nop" - -module IRB - module ExtendCommand - class ShowDoc < Nop - class << self - def transform_args(args) - # Return a string literal as is for backward compatibility - if args.empty? || string_literal?(args) - args - else # Otherwise, consider the input as a String for convenience - args.strip.dump - end - end - end - - category "Context" - description "Enter the mode to look up RI documents." - - def execute(*names) - require 'rdoc/ri/driver' - - unless ShowDoc.const_defined?(:Ri) - opts = RDoc::RI::Driver.process_args([]) - ShowDoc.const_set(:Ri, RDoc::RI::Driver.new(opts)) - end - - if names.empty? - Ri.interactive - else - names.each do |name| - begin - Ri.display_name(name.to_s) - rescue RDoc::RI::Error - puts $!.message - end - end - end - - nil - rescue LoadError, SystemExit - warn "Can't display document because `rdoc` is not installed." - end - end - end -end diff --git a/lib/irb/cmd/show_source.rb b/lib/irb/cmd/show_source.rb deleted file mode 100644 index 826cb11ed2..0000000000 --- a/lib/irb/cmd/show_source.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -require_relative "nop" -require_relative "../source_finder" -require_relative "../pager" -require_relative "../color" - -module IRB - module ExtendCommand - class ShowSource < Nop - category "Context" - description "Show the source code of a given method or constant." - - class << self - def transform_args(args) - # Return a string literal as is for backward compatibility - if args.empty? || string_literal?(args) - args - else # Otherwise, consider the input as a String for convenience - args.strip.dump - end - end - end - - def execute(str = nil) - unless str.is_a?(String) - puts "Error: Expected a string but got #{str.inspect}" - return - end - - str, esses = str.split(" -") - super_level = esses ? esses.count("s") : 0 - source = SourceFinder.new(@irb_context).find_source(str, super_level) - - if source - show_source(source) - elsif super_level > 0 - puts "Error: Couldn't locate a super definition for #{str}" - else - puts "Error: Couldn't locate a definition for #{str}" - end - nil - end - - private - - def show_source(source) - file_content = IRB::Color.colorize_code(File.read(source.file)) - code = file_content.lines[(source.first_line - 1)...source.last_line].join - content = <<~CONTENT - - #{bold("From")}: #{source.file}:#{source.first_line} - - #{code} - CONTENT - - Pager.page_content(content) - end - - def bold(str) - Color.colorize(str, [:BOLD]) - end - end - end -end diff --git a/lib/irb/color.rb b/lib/irb/color.rb index ad8670160c..fca942b28b 100644 --- a/lib/irb/color.rb +++ b/lib/irb/color.rb @@ -79,12 +79,12 @@ module IRB # :nodoc: class << self def colorable? - supported = $stdout.tty? && (/mswin|mingw/ =~ RUBY_PLATFORM || (ENV.key?('TERM') && ENV['TERM'] != 'dumb')) + supported = $stdout.tty? && (/mswin|mingw/.match?(RUBY_PLATFORM) || (ENV.key?('TERM') && ENV['TERM'] != 'dumb')) # because ruby/debug also uses irb's color module selectively, # irb won't be activated in that case. if IRB.respond_to?(:conf) - supported && IRB.conf.fetch(:USE_COLORIZE, true) + supported && !!IRB.conf.fetch(:USE_COLORIZE, true) else supported end diff --git a/lib/irb/command.rb b/lib/irb/command.rb new file mode 100644 index 0000000000..68a4b52727 --- /dev/null +++ b/lib/irb/command.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +# +# irb/command.rb - irb command +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# + +require_relative "command/base" + +module IRB # :nodoc: + module Command + @commands = {} + + class << self + attr_reader :commands + + # Registers a command with the given name. + # Aliasing is intentionally not supported at the moment. + def register(name, command_class) + @commands[name.to_sym] = [command_class, []] + end + end + end +end diff --git a/lib/irb/command/backtrace.rb b/lib/irb/command/backtrace.rb new file mode 100644 index 0000000000..687bb075ac --- /dev/null +++ b/lib/irb/command/backtrace.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Backtrace < DebugCommand + def execute(arg) + execute_debug_command(pre_cmds: "backtrace #{arg}") + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/base.rb b/lib/irb/command/base.rb new file mode 100644 index 0000000000..1d406630a2 --- /dev/null +++ b/lib/irb/command/base.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +# +# nop.rb - +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# + +module IRB + # :stopdoc: + + module Command + class CommandArgumentError < StandardError; end + + def self.extract_ruby_args(*args, **kwargs) + throw :EXTRACT_RUBY_ARGS, [args, kwargs] + end + + class Base + class << self + def category(category = nil) + @category = category if category + @category || "No category" + end + + def description(description = nil) + @description = description if description + @description || "No description provided." + end + + def help_message(help_message = nil) + @help_message = help_message if help_message + @help_message + end + + private + + def highlight(text) + Color.colorize(text, [:BOLD, :BLUE]) + end + end + + def self.execute(irb_context, arg) + new(irb_context).execute(arg) + rescue CommandArgumentError => e + puts e.message + end + + def initialize(irb_context) + @irb_context = irb_context + end + + attr_reader :irb_context + + def execute(arg) + #nop + end + end + + Nop = Base + end + + # :startdoc: +end diff --git a/lib/irb/command/break.rb b/lib/irb/command/break.rb new file mode 100644 index 0000000000..a8f81fe665 --- /dev/null +++ b/lib/irb/command/break.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Break < DebugCommand + def execute(arg) + execute_debug_command(pre_cmds: "break #{arg}") + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/catch.rb b/lib/irb/command/catch.rb new file mode 100644 index 0000000000..529dcbca5a --- /dev/null +++ b/lib/irb/command/catch.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Catch < DebugCommand + def execute(arg) + execute_debug_command(pre_cmds: "catch #{arg}") + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/chws.rb b/lib/irb/command/chws.rb new file mode 100644 index 0000000000..ef456d0961 --- /dev/null +++ b/lib/irb/command/chws.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +# +# change-ws.rb - +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# +require_relative "../ext/change-ws" + +module IRB + # :stopdoc: + + module Command + + class CurrentWorkingWorkspace < Base + category "Workspace" + description "Show the current workspace." + + def execute(_arg) + puts "Current workspace: #{irb_context.main}" + end + end + + class ChangeWorkspace < Base + category "Workspace" + description "Change the current workspace to an object." + + def execute(arg) + if arg.empty? + irb_context.change_workspace + else + obj = eval(arg, irb_context.workspace.binding) + irb_context.change_workspace(obj) + end + + puts "Current workspace: #{irb_context.main}" + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/context.rb b/lib/irb/command/context.rb new file mode 100644 index 0000000000..b4fc807343 --- /dev/null +++ b/lib/irb/command/context.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module IRB + module Command + class Context < Base + category "IRB" + description "Displays current configuration." + + def execute(_arg) + # This command just displays the configuration. + # Modifying the configuration is achieved by sending a message to IRB.conf. + Pager.page_content(IRB.CurrentContext.inspect) + end + end + end +end diff --git a/lib/irb/cmd/continue.rb b/lib/irb/command/continue.rb index 9136177eef..0daa029b15 100644 --- a/lib/irb/cmd/continue.rb +++ b/lib/irb/command/continue.rb @@ -5,10 +5,10 @@ require_relative "debug" module IRB # :stopdoc: - module ExtendCommand + module Command class Continue < DebugCommand - def execute(*args) - super(do_cmds: ["continue", *args].join(" ")) + def execute(arg) + execute_debug_command(do_cmds: "continue #{arg}") end end end diff --git a/lib/irb/cmd/debug.rb b/lib/irb/command/debug.rb index e236084ca4..8a091a49ed 100644 --- a/lib/irb/cmd/debug.rb +++ b/lib/irb/command/debug.rb @@ -1,20 +1,21 @@ -require_relative "nop" require_relative "../debug" module IRB # :stopdoc: - module ExtendCommand - class Debug < Nop + module Command + class Debug < Base category "Debugging" description "Start the debugger of debug.gem." - BINDING_IRB_FRAME_REGEXPS = [ - '<internal:prelude>', - binding.method(:irb).source_location.first, - ].map { |file| /\A#{Regexp.escape(file)}:\d+:in `irb'\z/ } + def execute(_arg) + execute_debug_command + end + + def execute_debug_command(pre_cmds: nil, do_cmds: nil) + pre_cmds = pre_cmds&.rstrip + do_cmds = do_cmds&.rstrip - def execute(pre_cmds: nil, do_cmds: nil) if irb_context.with_debugger # If IRB is already running with a debug session, throw the command and IRB.debug_readline will pass it to the debugger. if cmd = pre_cmds || do_cmds @@ -30,7 +31,7 @@ module IRB # 3. Insert a debug breakpoint at `Irb#debug_break` with the intended command. # 4. Exit the current Irb#run call via `throw :IRB_EXIT`. # 5. `Irb#debug_break` will be called and trigger the breakpoint, which will run the intended command. - unless binding_irb? + unless irb_context.from_binding? puts "Debugging commands are only available when IRB is started with binding.irb" return end @@ -54,16 +55,6 @@ module IRB throw :IRB_EXIT end end - - private - - def binding_irb? - caller.any? do |frame| - BINDING_IRB_FRAME_REGEXPS.any? do |regexp| - frame.match?(regexp) - end - end - end end class DebugCommand < Debug diff --git a/lib/irb/cmd/delete.rb b/lib/irb/command/delete.rb index aeb26d2572..2a57a4a3de 100644 --- a/lib/irb/cmd/delete.rb +++ b/lib/irb/command/delete.rb @@ -5,10 +5,10 @@ require_relative "debug" module IRB # :stopdoc: - module ExtendCommand + module Command class Delete < DebugCommand - def execute(*args) - super(pre_cmds: ["delete", *args].join(" ")) + def execute(arg) + execute_debug_command(pre_cmds: "delete #{arg}") end end end diff --git a/lib/irb/command/disable_irb.rb b/lib/irb/command/disable_irb.rb new file mode 100644 index 0000000000..0b00d0302b --- /dev/null +++ b/lib/irb/command/disable_irb.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module IRB + # :stopdoc: + + module Command + class DisableIrb < Base + category "IRB" + description "Disable binding.irb." + + def execute(*) + ::Binding.define_method(:irb) {} + IRB.irb_exit + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/edit.rb b/lib/irb/command/edit.rb new file mode 100644 index 0000000000..cb7e0c4873 --- /dev/null +++ b/lib/irb/command/edit.rb @@ -0,0 +1,63 @@ +require 'shellwords' + +require_relative "../color" +require_relative "../source_finder" + +module IRB + # :stopdoc: + + module Command + class Edit < Base + include RubyArgsExtractor + + category "Misc" + description 'Open a file or source location.' + help_message <<~HELP_MESSAGE + Usage: edit [FILE or constant or method signature] + + Open a file in the editor specified in #{highlight('ENV["VISUAL"]')} or #{highlight('ENV["EDITOR"]')} + + - If no arguments are provided, IRB will attempt to open the file the current context was defined in. + - If FILE is provided, IRB will open the file. + - If a constant or method signature is provided, IRB will attempt to locate the source file and open it. + + Examples: + + edit + edit foo.rb + edit Foo + edit Foo#bar + HELP_MESSAGE + + def execute(arg) + # Accept string literal for backward compatibility + path = unwrap_string_literal(arg) + + if path.nil? + path = @irb_context.irb_path + elsif !File.exist?(path) + source = SourceFinder.new(@irb_context).find_source(path) + + if source&.file_exist? && !source.binary_file? + path = source.file + end + end + + unless File.exist?(path) + puts "Can not find file: #{path}" + return + end + + if editor = (ENV['VISUAL'] || ENV['EDITOR']) + puts "command: '#{editor}'" + puts " path: #{path}" + system(*Shellwords.split(editor), path) + else + puts "Can not find editor setting: ENV['VISUAL'] or ENV['EDITOR']" + end + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/exit.rb b/lib/irb/command/exit.rb new file mode 100644 index 0000000000..b4436f0343 --- /dev/null +++ b/lib/irb/command/exit.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module IRB + # :stopdoc: + + module Command + class Exit < Base + category "IRB" + description "Exit the current irb session." + + def execute(_arg) + IRB.irb_exit + end + end + end + + # :startdoc: +end diff --git a/lib/irb/cmd/finish.rb b/lib/irb/command/finish.rb index 29f100feb5..3311a0e6e9 100644 --- a/lib/irb/cmd/finish.rb +++ b/lib/irb/command/finish.rb @@ -5,10 +5,10 @@ require_relative "debug" module IRB # :stopdoc: - module ExtendCommand + module Command class Finish < DebugCommand - def execute(*args) - super(do_cmds: ["finish", *args].join(" ")) + def execute(arg) + execute_debug_command(do_cmds: "finish #{arg}") end end end diff --git a/lib/irb/command/force_exit.rb b/lib/irb/command/force_exit.rb new file mode 100644 index 0000000000..14086aa849 --- /dev/null +++ b/lib/irb/command/force_exit.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module IRB + # :stopdoc: + + module Command + class ForceExit < Base + category "IRB" + description "Exit the current process." + + def execute(_arg) + throw :IRB_EXIT, true + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/help.rb b/lib/irb/command/help.rb new file mode 100644 index 0000000000..c2018f9b30 --- /dev/null +++ b/lib/irb/command/help.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module IRB + module Command + class Help < Base + category "Help" + description "List all available commands. Use `help <command>` to get information about a specific command." + + def execute(command_name) + content = + if command_name.empty? + help_message + else + if command_class = Command.load_command(command_name) + command_class.help_message || command_class.description + else + "Can't find command `#{command_name}`. Please check the command name and try again.\n\n" + end + end + Pager.page_content(content) + end + + private + + def help_message + commands_info = IRB::Command.all_commands_info + helper_methods_info = IRB::HelperMethod.all_helper_methods_info + commands_grouped_by_categories = commands_info.group_by { |cmd| cmd[:category] } + commands_grouped_by_categories["Helper methods"] = helper_methods_info + + if irb_context.with_debugger + # Remove the original "Debugging" category + commands_grouped_by_categories.delete("Debugging") + end + + longest_cmd_name_length = commands_info.map { |c| c[:display_name].length }.max + + output = StringIO.new + + help_cmds = commands_grouped_by_categories.delete("Help") + no_category_cmds = commands_grouped_by_categories.delete("No category") + aliases = irb_context.instance_variable_get(:@user_aliases).map do |alias_name, target| + { display_name: alias_name, description: "Alias for `#{target}`" } + end + + # Display help commands first + add_category_to_output("Help", help_cmds, output, longest_cmd_name_length) + + # Display the rest of the commands grouped by categories + commands_grouped_by_categories.each do |category, cmds| + add_category_to_output(category, cmds, output, longest_cmd_name_length) + end + + # Display commands without a category + if no_category_cmds + add_category_to_output("No category", no_category_cmds, output, longest_cmd_name_length) + end + + # Display aliases + add_category_to_output("Aliases", aliases, output, longest_cmd_name_length) + + # Append the debugger help at the end + if irb_context.with_debugger + # Add "Debugging (from debug.gem)" category as title + add_category_to_output("Debugging (from debug.gem)", [], output, longest_cmd_name_length) + output.puts DEBUGGER__.help + end + + output.string + end + + def add_category_to_output(category, cmds, output, longest_cmd_name_length) + output.puts Color.colorize(category, [:BOLD]) + + cmds.each do |cmd| + output.puts " #{cmd[:display_name].to_s.ljust(longest_cmd_name_length)} #{cmd[:description]}" + end + + output.puts + end + end + end +end diff --git a/lib/irb/cmd/history.rb b/lib/irb/command/history.rb index 5b712fa44d..90f87f9102 100644 --- a/lib/irb/cmd/history.rb +++ b/lib/irb/command/history.rb @@ -1,25 +1,23 @@ # frozen_string_literal: true require "stringio" -require_relative "nop" + require_relative "../pager" module IRB # :stopdoc: - module ExtendCommand - class History < Nop + module Command + class History < Base category "IRB" description "Shows the input history. `-g [query]` or `-G [query]` allows you to filter the output." - def self.transform_args(args) - match = args&.match(/(-g|-G)\s+(?<grep>.+)\s*\n\z/) - return unless match + def execute(arg) - "grep: #{Regexp.new(match[:grep]).inspect}" - end + if (match = arg&.match(/(-g|-G)\s+(?<grep>.+)\s*\n\z/)) + grep = Regexp.new(match[:grep]) + end - def execute(grep: nil) formatted_inputs = irb_context.io.class::HISTORY.each_with_index.reverse_each.filter_map do |input, index| next if grep && !input.match?(grep) diff --git a/lib/irb/command/info.rb b/lib/irb/command/info.rb new file mode 100644 index 0000000000..d08ce00a32 --- /dev/null +++ b/lib/irb/command/info.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "debug" + +module IRB + # :stopdoc: + + module Command + class Info < DebugCommand + def execute(arg) + execute_debug_command(pre_cmds: "info #{arg}") + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/internal_helpers.rb b/lib/irb/command/internal_helpers.rb new file mode 100644 index 0000000000..249b5cdede --- /dev/null +++ b/lib/irb/command/internal_helpers.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module IRB + module Command + # Internal use only, for default command's backward compatibility. + module RubyArgsExtractor # :nodoc: + def unwrap_string_literal(str) + return if str.empty? + + sexp = Ripper.sexp(str) + if sexp && sexp.size == 2 && sexp.last&.first&.first == :string_literal + @irb_context.workspace.binding.eval(str).to_s + else + str + end + end + + def ruby_args(arg) + # Use throw and catch to handle arg that includes `;` + # For example: "1, kw: (2; 3); 4" will be parsed to [[1], { kw: 3 }] + catch(:EXTRACT_RUBY_ARGS) do + @irb_context.workspace.binding.eval "IRB::Command.extract_ruby_args #{arg}" + end || [[], {}] + end + end + end +end diff --git a/lib/irb/cmd/irb_info.rb b/lib/irb/command/irb_info.rb index 5b905a09bd..6d868de94c 100644 --- a/lib/irb/cmd/irb_info.rb +++ b/lib/irb/command/irb_info.rb @@ -1,21 +1,20 @@ -# frozen_string_literal: false - -require_relative "nop" +# frozen_string_literal: true module IRB # :stopdoc: - module ExtendCommand - class IrbInfo < Nop + module Command + class IrbInfo < Base category "IRB" description "Show information about IRB." - def execute + def execute(_arg) str = "Ruby version: #{RUBY_VERSION}\n" str += "IRB version: #{IRB.version}\n" str += "InputMethod: #{IRB.CurrentContext.io.inspect}\n" str += "Completion: #{IRB.CurrentContext.io.respond_to?(:completion_info) ? IRB.CurrentContext.io.completion_info : 'off'}\n" - str += ".irbrc path: #{IRB.rc_file}\n" if File.exist?(IRB.rc_file) + rc_files = IRB.irbrc_files + str += ".irbrc paths: #{rc_files.join(", ")}\n" if rc_files.any? str += "RUBY_PLATFORM: #{RUBY_PLATFORM}\n" str += "LANG env: #{ENV["LANG"]}\n" if ENV["LANG"] && !ENV["LANG"].empty? str += "LC_ALL env: #{ENV["LC_ALL"]}\n" if ENV["LC_ALL"] && !ENV["LC_ALL"].empty? diff --git a/lib/irb/cmd/load.rb b/lib/irb/command/load.rb index a3e797a7e0..1cd3f279d1 100644 --- a/lib/irb/cmd/load.rb +++ b/lib/irb/command/load.rb @@ -1,17 +1,16 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # load.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) # - -require_relative "nop" require_relative "../ext/loader" module IRB # :stopdoc: - module ExtendCommand - class LoaderCommand < Nop + module Command + class LoaderCommand < Base + include RubyArgsExtractor include IrbLoader def raise_cmd_argument_error @@ -23,7 +22,12 @@ module IRB category "IRB" description "Load a Ruby file." - def execute(file_name = nil, priv = nil) + def execute(arg) + args, kwargs = ruby_args(arg) + execute_internal(*args, **kwargs) + end + + def execute_internal(file_name = nil, priv = nil) raise_cmd_argument_error unless file_name irb_load(file_name, priv) end @@ -32,7 +36,13 @@ module IRB class Require < LoaderCommand category "IRB" description "Require a Ruby file." - def execute(file_name = nil) + + def execute(arg) + args, kwargs = ruby_args(arg) + execute_internal(*args, **kwargs) + end + + def execute_internal(file_name = nil) raise_cmd_argument_error unless file_name rex = Regexp.new("#{Regexp.quote(file_name)}(\.o|\.rb)?") @@ -65,7 +75,12 @@ module IRB category "IRB" description "Loads a given file in the current session." - def execute(file_name = nil) + def execute(arg) + args, kwargs = ruby_args(arg) + execute_internal(*args, **kwargs) + end + + def execute_internal(file_name = nil) raise_cmd_argument_error unless file_name source_file(file_name) diff --git a/lib/irb/cmd/ls.rb b/lib/irb/command/ls.rb index 791b1c1b21..cbd9998bc4 100644 --- a/lib/irb/cmd/ls.rb +++ b/lib/irb/command/ls.rb @@ -2,39 +2,55 @@ require "reline" require "stringio" -require_relative "nop" + require_relative "../pager" require_relative "../color" module IRB # :stopdoc: - module ExtendCommand - class Ls < Nop + module Command + class Ls < Base + include RubyArgsExtractor + category "Context" - description "Show methods, constants, and variables. `-g [query]` or `-G [query]` allows you to filter out the output." + description "Show methods, constants, and variables." + + help_message <<~HELP_MESSAGE + Usage: ls [obj] [-g [query]] + + -g [query] Filter the output with a query. + HELP_MESSAGE - def self.transform_args(args) - if match = args&.match(/\A(?<args>.+\s|)(-g|-G)\s+(?<grep>[^\s]+)\s*\n\z/) - args = match[:args] - "#{args}#{',' unless args.chomp.empty?} grep: /#{match[:grep]}/" + def execute(arg) + if match = arg.match(/\A(?<target>.+\s|)(-g|-G)\s+(?<grep>.+)$/) + if match[:target].empty? + use_main = true + else + obj = @irb_context.workspace.binding.eval(match[:target]) + end + grep = Regexp.new(match[:grep]) else - args + args, kwargs = ruby_args(arg) + use_main = args.empty? + obj = args.first + grep = kwargs[:grep] + end + + if use_main + obj = irb_context.workspace.main + locals = irb_context.workspace.binding.local_variables end - end - def execute(*arg, grep: nil) o = Output.new(grep: grep) - obj = arg.empty? ? irb_context.workspace.main : arg.first - locals = arg.empty? ? irb_context.workspace.binding.local_variables : [] klass = (obj.class == Class || obj.class == Module ? obj : obj.class) o.dump("constants", obj.constants) if obj.respond_to?(:constants) dump_methods(o, klass, obj) o.dump("instance variables", obj.instance_variables) o.dump("class variables", klass.class_variables) - o.dump("locals", locals) + o.dump("locals", locals) if locals o.print_result end diff --git a/lib/irb/cmd/measure.rb b/lib/irb/command/measure.rb index 4e1125a0a6..f96be20de8 100644 --- a/lib/irb/cmd/measure.rb +++ b/lib/irb/command/measure.rb @@ -1,10 +1,10 @@ -require_relative "nop" - module IRB # :stopdoc: - module ExtendCommand - class Measure < Nop + module Command + class Measure < Base + include RubyArgsExtractor + category "Misc" description "`measure` enables the mode to measure processing time. `measure :off` disables it." @@ -12,15 +12,19 @@ module IRB super(*args) end - def execute(type = nil, arg = nil) - # Please check IRB.init_config in lib/irb/init.rb that sets - # IRB.conf[:MEASURE_PROC] to register default "measure" methods, - # "measure :time" (abbreviated as "measure") and "measure :stackprof". - - if block_given? + def execute(arg) + if arg&.match?(/^do$|^do[^\w]|^\{/) warn 'Configure IRB.conf[:MEASURE_PROC] to add custom measure methods.' return end + args, kwargs = ruby_args(arg) + execute_internal(*args, **kwargs) + end + + def execute_internal(type = nil, arg = nil) + # Please check IRB.init_config in lib/irb/init.rb that sets + # IRB.conf[:MEASURE_PROC] to register default "measure" methods, + # "measure :time" (abbreviated as "measure") and "measure :stackprof". case type when :off diff --git a/lib/irb/cmd/next.rb b/lib/irb/command/next.rb index d29c82e7fc..3fc6b68d21 100644 --- a/lib/irb/cmd/next.rb +++ b/lib/irb/command/next.rb @@ -5,10 +5,10 @@ require_relative "debug" module IRB # :stopdoc: - module ExtendCommand + module Command class Next < DebugCommand - def execute(*args) - super(do_cmds: ["next", *args].join(" ")) + def execute(arg) + execute_debug_command(do_cmds: "next #{arg}") end end end diff --git a/lib/irb/command/pushws.rb b/lib/irb/command/pushws.rb new file mode 100644 index 0000000000..b51928c650 --- /dev/null +++ b/lib/irb/command/pushws.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true +# +# change-ws.rb - +# by Keiju ISHITSUKA(keiju@ruby-lang.org) +# + +require_relative "../ext/workspaces" + +module IRB + # :stopdoc: + + module Command + class Workspaces < Base + category "Workspace" + description "Show workspaces." + + def execute(_arg) + inspection_resuls = irb_context.instance_variable_get(:@workspace_stack).map do |ws| + truncated_inspect(ws.main) + end + + puts "[" + inspection_resuls.join(", ") + "]" + end + + private + + def truncated_inspect(obj) + obj_inspection = obj.inspect + + if obj_inspection.size > 20 + obj_inspection = obj_inspection[0, 19] + "...>" + end + + obj_inspection + end + end + + class PushWorkspace < Workspaces + category "Workspace" + description "Push an object to the workspace stack." + + def execute(arg) + if arg.empty? + irb_context.push_workspace + else + obj = eval(arg, irb_context.workspace.binding) + irb_context.push_workspace(obj) + end + super + end + end + + class PopWorkspace < Workspaces + category "Workspace" + description "Pop a workspace from the workspace stack." + + def execute(_arg) + irb_context.pop_workspace + super + end + end + end + + # :startdoc: +end diff --git a/lib/irb/command/show_doc.rb b/lib/irb/command/show_doc.rb new file mode 100644 index 0000000000..8a2188e4eb --- /dev/null +++ b/lib/irb/command/show_doc.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module IRB + module Command + class ShowDoc < Base + include RubyArgsExtractor + + category "Context" + description "Look up documentation with RI." + + help_message <<~HELP_MESSAGE + Usage: show_doc [name] + + When name is provided, IRB will look up the documentation for the given name. + When no name is provided, a RI session will be started. + + Examples: + + show_doc + show_doc Array + show_doc Array#each + + HELP_MESSAGE + + def execute(arg) + # Accept string literal for backward compatibility + name = unwrap_string_literal(arg) + require 'rdoc/ri/driver' + + unless ShowDoc.const_defined?(:Ri) + opts = RDoc::RI::Driver.process_args([]) + ShowDoc.const_set(:Ri, RDoc::RI::Driver.new(opts)) + end + + if name.nil? + Ri.interactive + else + begin + Ri.display_name(name) + rescue RDoc::RI::Error + puts $!.message + end + end + + nil + rescue LoadError, SystemExit + warn "Can't display document because `rdoc` is not installed." + end + end + end +end diff --git a/lib/irb/command/show_source.rb b/lib/irb/command/show_source.rb new file mode 100644 index 0000000000..f4c6f104a2 --- /dev/null +++ b/lib/irb/command/show_source.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require_relative "../source_finder" +require_relative "../pager" +require_relative "../color" + +module IRB + module Command + class ShowSource < Base + include RubyArgsExtractor + + category "Context" + description "Show the source code of a given method, class/module, or constant." + + help_message <<~HELP_MESSAGE + Usage: show_source [target] [-s] + + -s Show the super method. You can stack it like `-ss` to show the super of the super, etc. + + Examples: + + show_source Foo + show_source Foo#bar + show_source Foo#bar -s + show_source Foo.baz + show_source Foo::BAR + HELP_MESSAGE + + def execute(arg) + # Accept string literal for backward compatibility + str = unwrap_string_literal(arg) + unless str.is_a?(String) + puts "Error: Expected a string but got #{str.inspect}" + return + end + + str, esses = str.split(" -") + super_level = esses ? esses.count("s") : 0 + source = SourceFinder.new(@irb_context).find_source(str, super_level) + + if source + show_source(source) + elsif super_level > 0 + puts "Error: Couldn't locate a super definition for #{str}" + else + puts "Error: Couldn't locate a definition for #{str}" + end + nil + end + + private + + def show_source(source) + if source.binary_file? + content = "\n#{bold('Defined in binary file')}: #{source.file}\n\n" + else + code = source.colorized_content || 'Source not available' + content = <<~CONTENT + + #{bold("From")}: #{source.file}:#{source.line} + + #{code.chomp} + + CONTENT + end + Pager.page_content(content) + end + + def bold(str) + Color.colorize(str, [:BOLD]) + end + end + end +end diff --git a/lib/irb/cmd/step.rb b/lib/irb/command/step.rb index 2bc74a9d79..29e5e35ac0 100644 --- a/lib/irb/cmd/step.rb +++ b/lib/irb/command/step.rb @@ -5,10 +5,10 @@ require_relative "debug" module IRB # :stopdoc: - module ExtendCommand + module Command class Step < DebugCommand - def execute(*args) - super(do_cmds: ["step", *args].join(" ")) + def execute(arg) + execute_debug_command(do_cmds: "step #{arg}") end end end diff --git a/lib/irb/cmd/subirb.rb b/lib/irb/command/subirb.rb index 5ffd646416..85af28c1a5 100644 --- a/lib/irb/cmd/subirb.rb +++ b/lib/irb/command/subirb.rb @@ -1,19 +1,15 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # multi.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) # -require_relative "nop" - module IRB # :stopdoc: - module ExtendCommand - class MultiIRBCommand < Nop - def execute(*args) - extend_irb_context - end + module Command + class MultiIRBCommand < Base + include RubyArgsExtractor private @@ -38,7 +34,12 @@ module IRB category "Multi-irb (DEPRECATED)" description "Start a child IRB." - def execute(*obj) + def execute(arg) + args, kwargs = ruby_args(arg) + execute_internal(*args, **kwargs) + end + + def execute_internal(*obj) print_deprecated_warning if irb_context.with_debugger @@ -46,8 +47,9 @@ module IRB return end - super + extend_irb_context IRB.irb(nil, *obj) + puts IRB.JobManager.inspect end end @@ -55,7 +57,7 @@ module IRB category "Multi-irb (DEPRECATED)" description "List of current sessions." - def execute + def execute(_arg) print_deprecated_warning if irb_context.with_debugger @@ -63,8 +65,8 @@ module IRB return end - super - IRB.JobManager + extend_irb_context + puts IRB.JobManager.inspect end end @@ -72,7 +74,12 @@ module IRB category "Multi-irb (DEPRECATED)" description "Switches to the session of the given number." - def execute(key = nil) + def execute(arg) + args, kwargs = ruby_args(arg) + execute_internal(*args, **kwargs) + end + + def execute_internal(key = nil) print_deprecated_warning if irb_context.with_debugger @@ -80,10 +87,11 @@ module IRB return end - super + extend_irb_context raise CommandArgumentError.new("Please specify the id of target IRB job (listed in the `jobs` command).") unless key IRB.JobManager.switch(key) + puts IRB.JobManager.inspect end end @@ -91,7 +99,12 @@ module IRB category "Multi-irb (DEPRECATED)" description "Kills the session with the given number." - def execute(*keys) + def execute(arg) + args, kwargs = ruby_args(arg) + execute_internal(*args, **kwargs) + end + + def execute_internal(*keys) print_deprecated_warning if irb_context.with_debugger @@ -99,8 +112,9 @@ module IRB return end - super + extend_irb_context IRB.JobManager.kill(*keys) + puts IRB.JobManager.inspect end end end diff --git a/lib/irb/cmd/whereami.rb b/lib/irb/command/whereami.rb index 8f56ba073d..c8439f1212 100644 --- a/lib/irb/cmd/whereami.rb +++ b/lib/irb/command/whereami.rb @@ -1,16 +1,14 @@ # frozen_string_literal: true -require_relative "nop" - module IRB # :stopdoc: - module ExtendCommand - class Whereami < Nop + module Command + class Whereami < Base category "Context" description "Show the source code around binding.irb again." - def execute(*) + def execute(_arg) code = irb_context.workspace.code_around_binding if code puts code diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index af3b69eb27..a3d89373c3 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/completion.rb - # by Keiju ISHITSUKA(keiju@ishitsuka.com) @@ -86,6 +86,14 @@ module IRB ) end + def command_completions(preposing, target) + if preposing.empty? && !target.empty? + IRB::Command.command_names.select { _1.start_with?(target) } + else + [] + end + end + def retrieve_files_to_require_relative_from_current_dir @files_from_current_dir ||= Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: '.').map { |path| path.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '') @@ -103,9 +111,11 @@ module IRB end def completion_candidates(preposing, target, _postposing, bind:) + commands = command_completions(preposing, target) result = ReplTypeCompletor.analyze(preposing + target, binding: bind, filename: @context.irb_path) - return [] unless result - result.completion_candidates.map { target + _1 } + return commands unless result + + commands | result.completion_candidates.map { target + _1 } end def doc_namespace(preposing, matched, _postposing, bind:) @@ -181,7 +191,8 @@ module IRB result = complete_require_path(target, preposing, postposing) return result if result end - retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.map{ |i| i.encode(Encoding.default_external) } + commands = command_completions(preposing || '', target) + commands | retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.map{ |i| i.encode(Encoding.default_external) } end def doc_namespace(_preposing, matched, _postposing, bind:) @@ -388,7 +399,7 @@ module IRB if doc_namespace rec_class = rec.is_a?(Module) ? rec : rec.class - "#{rec_class.name}#{sep}#{candidates.find{ |i| i == message }}" + "#{rec_class.name}#{sep}#{candidates.find{ |i| i == message }}" rescue nil else select_message(receiver, message, candidates, sep) end @@ -406,13 +417,19 @@ module IRB else select_message(receiver, message, candidates.sort) end - + when /^\s*$/ + # empty input + if doc_namespace + nil + else + [] + end else if doc_namespace vars = (bind.local_variables | bind.eval_instance_variables).collect{|m| m.to_s} perfect_match_var = vars.find{|m| m.to_s == input} if perfect_match_var - eval("#{perfect_match_var}.class.name", bind) + eval("#{perfect_match_var}.class.name", bind) rescue nil else candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s} candidates |= ReservedWords diff --git a/lib/irb/context.rb b/lib/irb/context.rb index c3690fcac7..5d2ff97328 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/context.rb - irb context # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -22,10 +22,11 @@ module IRB # +other+:: uses this as InputMethod def initialize(irb, workspace = nil, input_method = nil) @irb = irb + @workspace_stack = [] if workspace - @workspace = workspace + @workspace_stack << workspace else - @workspace = WorkSpace.new + @workspace_stack << WorkSpace.new end @thread = Thread.current @@ -61,7 +62,7 @@ module IRB @io = nil self.inspect_mode = IRB.conf[:INSPECT_MODE] - self.use_tracer = IRB.conf[:USE_TRACER] if IRB.conf[:USE_TRACER] + self.use_tracer = IRB.conf[:USE_TRACER] self.use_loader = IRB.conf[:USE_LOADER] if IRB.conf[:USE_LOADER] self.eval_history = IRB.conf[:EVAL_HISTORY] if IRB.conf[:EVAL_HISTORY] @@ -77,14 +78,14 @@ module IRB else @irb_name = IRB.conf[:IRB_NAME]+"#"+IRB.JobManager.n_jobs.to_s end - @irb_path = "(" + @irb_name + ")" + self.irb_path = "(" + @irb_name + ")" case input_method when nil @io = nil case use_multiline? when nil - if STDIN.tty? && IRB.conf[:PROMPT_MODE] != :INF_RUBY && !use_singleline? + if term_interactive? && IRB.conf[:PROMPT_MODE] != :INF_RUBY && !use_singleline? # Both of multiline mode and singleline mode aren't specified. @io = RelineInputMethod.new(build_completor) else @@ -98,7 +99,7 @@ module IRB unless @io case use_singleline? when nil - if (defined?(ReadlineInputMethod) && STDIN.tty? && + if (defined?(ReadlineInputMethod) && term_interactive? && IRB.conf[:PROMPT_MODE] != :INF_RUBY) @io = ReadlineInputMethod.new else @@ -121,11 +122,11 @@ module IRB when '-' @io = FileInputMethod.new($stdin) @irb_name = '-' - @irb_path = '-' + self.irb_path = '-' when String @io = FileInputMethod.new(input_method) @irb_name = File.basename(input_method) - @irb_path = input_method + self.irb_path = input_method else @io = input_method end @@ -150,6 +151,11 @@ module IRB @command_aliases = @user_aliases.merge(KEYWORD_ALIASES) end + private def term_interactive? + return true if ENV['TEST_IRB_FORCE_INTERACTIVE'] + STDIN.tty? && ENV['TERM'] != 'dumb' + end + # because all input will eventually be evaluated as Ruby code, # command names that conflict with Ruby keywords need special workaround # we can remove them once we implemented a better command system for IRB @@ -161,6 +167,23 @@ module IRB private_constant :KEYWORD_ALIASES + def use_tracer=(val) + require_relative "ext/tracer" if val + IRB.conf[:USE_TRACER] = val + end + + def eval_history=(val) + self.class.remove_method(__method__) + require_relative "ext/eval_history" + __send__(__method__, val) + end + + def use_loader=(val) + self.class.remove_method(__method__) + require_relative "ext/use-loader" + __send__(__method__, val) + end + private def build_completor completor_type = IRB.conf[:COMPLETOR] case completor_type @@ -178,7 +201,7 @@ module IRB private def build_type_completor if RUBY_ENGINE == 'truffleruby' - # Avoid SynatxError. truffleruby does not support endless method definition yet. + # Avoid SyntaxError. truffleruby does not support endless method definition yet. warn 'TypeCompletor is not supported on TruffleRuby yet' return end @@ -212,15 +235,24 @@ module IRB IRB.conf[:HISTORY_FILE] = hist end + # Workspace in the current context. + def workspace + @workspace_stack.last + end + + # Replace the current workspace with the given +workspace+. + def replace_workspace(workspace) + @workspace_stack.pop + @workspace_stack.push(workspace) + end + # The top-level workspace, see WorkSpace#main def main - @workspace.main + workspace.main end # The toplevel workspace, see #home_workspace attr_reader :workspace_home - # WorkSpace in the current context. - attr_accessor :workspace # The current thread in this context. attr_reader :thread # The current input method. @@ -241,9 +273,27 @@ module IRB # Can be either name from <code>IRB.conf[:IRB_NAME]</code>, or the number of # the current job set by JobManager, such as <code>irb#2</code> attr_accessor :irb_name - # Can be either the #irb_name surrounded by parenthesis, or the - # +input_method+ passed to Context.new - attr_accessor :irb_path + + # Can be one of the following: + # - the #irb_name surrounded by parenthesis + # - the +input_method+ passed to Context.new + # - the file path of the current IRB context in a binding.irb session + attr_reader :irb_path + + # Sets @irb_path to the given +path+ as well as @eval_path + # @eval_path is used for evaluating code in the context of IRB session + # It's the same as irb_path, but with the IRB name postfix + # This makes sure users can distinguish the methods defined in the IRB session + # from the methods defined in the current file's context, especially with binding.irb + def irb_path=(path) + @irb_path = path + + if File.exist?(path) + @eval_path = "#{path}(#{IRB.conf[:IRB_NAME]})" + else + @eval_path = path + end + end # Whether multiline editor mode is enabled or not. # @@ -444,9 +494,7 @@ module IRB # StdioInputMethod or RelineInputMethod or ReadlineInputMethod, see #io # for more information. def prompting? - verbose? || (STDIN.tty? && @io.kind_of?(StdioInputMethod) || - @io.kind_of?(RelineInputMethod) || - (defined?(ReadlineInputMethod) && @io.kind_of?(ReadlineInputMethod))) + verbose? || @io.prompting? end # The return value of the last statement evaluated. @@ -456,7 +504,7 @@ module IRB # to #last_value. def set_last_value(value) @last_value = value - @workspace.local_variable_set :_, value + workspace.local_variable_set :_, value end # Sets the +mode+ of the prompt in this context. @@ -542,45 +590,55 @@ module IRB @inspect_mode end - def evaluate(line, line_no) # :nodoc: + def evaluate(statement, line_no) # :nodoc: @line_no = line_no - result = nil + case statement + when Statement::EmptyInput + return + when Statement::Expression + result = evaluate_expression(statement.code, line_no) + set_last_value(result) + when Statement::Command + statement.command_class.execute(self, statement.arg) + set_last_value(nil) + end + + nil + end + + def from_binding? + @irb.from_binding + end + + def evaluate_expression(code, line_no) # :nodoc: + result = nil if IRB.conf[:MEASURE] && IRB.conf[:MEASURE_CALLBACKS].empty? IRB.set_measure_callback end if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty? last_proc = proc do - result = @workspace.evaluate(line, irb_path, line_no) + result = workspace.evaluate(code, @eval_path, line_no) end IRB.conf[:MEASURE_CALLBACKS].inject(last_proc) do |chain, item| _name, callback, arg = item proc do - callback.(self, line, line_no, arg) do + callback.(self, code, line_no, arg) do chain.call end end end.call else - result = @workspace.evaluate(line, irb_path, line_no) + result = workspace.evaluate(code, @eval_path, line_no) end - - set_last_value(result) + result end def inspect_last_value # :nodoc: @inspect_method.inspect_value(@last_value) end - alias __exit__ exit - # Exits the current session, see IRB.irb_exit - def exit(ret = 0) - IRB.irb_exit(@irb, ret) - rescue UncaughtThrowError - super - end - NOPRINTING_IVARS = ["@last_value"] # :nodoc: NO_INSPECTING_IVARS = ["@irb", "@io"] # :nodoc: IDNAME_IVARS = ["@prompt_mode"] # :nodoc: @@ -611,17 +669,5 @@ module IRB def local_variables # :nodoc: workspace.binding.local_variables end - - # Return true if it's aliased from the argument and it's not an identifier. - def symbol_alias?(command) - return nil if command.match?(/\A\w+\z/) - command_aliases.key?(command.to_sym) - end - - # Return true if the command supports transforming args - def transform_args?(command) - command = command_aliases.fetch(command.to_sym, command) - ExtendCommandBundle.load_command(command)&.respond_to?(:transform_args) - end end end diff --git a/lib/irb/default_commands.rb b/lib/irb/default_commands.rb new file mode 100644 index 0000000000..1bbc68efa7 --- /dev/null +++ b/lib/irb/default_commands.rb @@ -0,0 +1,260 @@ +# frozen_string_literal: true + +require_relative "command" +require_relative "command/internal_helpers" +require_relative "command/context" +require_relative "command/exit" +require_relative "command/force_exit" +require_relative "command/chws" +require_relative "command/pushws" +require_relative "command/subirb" +require_relative "command/load" +require_relative "command/debug" +require_relative "command/edit" +require_relative "command/break" +require_relative "command/catch" +require_relative "command/next" +require_relative "command/delete" +require_relative "command/step" +require_relative "command/continue" +require_relative "command/finish" +require_relative "command/backtrace" +require_relative "command/info" +require_relative "command/help" +require_relative "command/show_doc" +require_relative "command/irb_info" +require_relative "command/ls" +require_relative "command/measure" +require_relative "command/show_source" +require_relative "command/whereami" +require_relative "command/history" + +module IRB + module Command + NO_OVERRIDE = 0 + OVERRIDE_PRIVATE_ONLY = 0x01 + OVERRIDE_ALL = 0x02 + + class << self + # This API is for IRB's internal use only and may change at any time. + # Please do NOT use it. + def _register_with_aliases(name, command_class, *aliases) + @commands[name.to_sym] = [command_class, aliases] + end + + def all_commands_info + user_aliases = IRB.CurrentContext.command_aliases.each_with_object({}) do |(alias_name, target), result| + result[target] ||= [] + result[target] << alias_name + end + + commands.map do |command_name, (command_class, aliases)| + aliases = aliases.map { |a| a.first } + + if additional_aliases = user_aliases[command_name] + aliases += additional_aliases + end + + display_name = aliases.shift || command_name + { + display_name: display_name, + description: command_class.description, + category: command_class.category + } + end + end + + def command_override_policies + @@command_override_policies ||= commands.flat_map do |cmd_name, (cmd_class, aliases)| + [[cmd_name, OVERRIDE_ALL]] + aliases + end.to_h + end + + def execute_as_command?(name, public_method:, private_method:) + case command_override_policies[name] + when OVERRIDE_ALL + true + when OVERRIDE_PRIVATE_ONLY + !public_method + when NO_OVERRIDE + !public_method && !private_method + end + end + + def command_names + command_override_policies.keys.map(&:to_s) + end + + # Convert a command name to its implementation class if such command exists + def load_command(command) + command = command.to_sym + commands.each do |command_name, (command_class, aliases)| + if command_name == command || aliases.any? { |alias_name, _| alias_name == command } + return command_class + end + end + nil + end + end + + _register_with_aliases(:irb_context, Command::Context, + [:context, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_exit, Command::Exit, + [:exit, OVERRIDE_PRIVATE_ONLY], + [:quit, OVERRIDE_PRIVATE_ONLY], + [:irb_quit, OVERRIDE_PRIVATE_ONLY] + ) + + _register_with_aliases(:irb_exit!, Command::ForceExit, + [:exit!, OVERRIDE_PRIVATE_ONLY] + ) + + _register_with_aliases(:irb_current_working_workspace, Command::CurrentWorkingWorkspace, + [:cwws, NO_OVERRIDE], + [:pwws, NO_OVERRIDE], + [:irb_print_working_workspace, OVERRIDE_ALL], + [:irb_cwws, OVERRIDE_ALL], + [:irb_pwws, OVERRIDE_ALL], + [:irb_current_working_binding, OVERRIDE_ALL], + [:irb_print_working_binding, OVERRIDE_ALL], + [:irb_cwb, OVERRIDE_ALL], + [:irb_pwb, OVERRIDE_ALL], + ) + + _register_with_aliases(:irb_change_workspace, Command::ChangeWorkspace, + [:chws, NO_OVERRIDE], + [:cws, NO_OVERRIDE], + [:irb_chws, OVERRIDE_ALL], + [:irb_cws, OVERRIDE_ALL], + [:irb_change_binding, OVERRIDE_ALL], + [:irb_cb, OVERRIDE_ALL], + [:cb, NO_OVERRIDE], + ) + + _register_with_aliases(:irb_workspaces, Command::Workspaces, + [:workspaces, NO_OVERRIDE], + [:irb_bindings, OVERRIDE_ALL], + [:bindings, NO_OVERRIDE], + ) + + _register_with_aliases(:irb_push_workspace, Command::PushWorkspace, + [:pushws, NO_OVERRIDE], + [:irb_pushws, OVERRIDE_ALL], + [:irb_push_binding, OVERRIDE_ALL], + [:irb_pushb, OVERRIDE_ALL], + [:pushb, NO_OVERRIDE], + ) + + _register_with_aliases(:irb_pop_workspace, Command::PopWorkspace, + [:popws, NO_OVERRIDE], + [:irb_popws, OVERRIDE_ALL], + [:irb_pop_binding, OVERRIDE_ALL], + [:irb_popb, OVERRIDE_ALL], + [:popb, NO_OVERRIDE], + ) + + _register_with_aliases(:irb_load, Command::Load) + _register_with_aliases(:irb_require, Command::Require) + _register_with_aliases(:irb_source, Command::Source, + [:source, NO_OVERRIDE] + ) + + _register_with_aliases(:irb, Command::IrbCommand) + _register_with_aliases(:irb_jobs, Command::Jobs, + [:jobs, NO_OVERRIDE] + ) + _register_with_aliases(:irb_fg, Command::Foreground, + [:fg, NO_OVERRIDE] + ) + _register_with_aliases(:irb_kill, Command::Kill, + [:kill, OVERRIDE_PRIVATE_ONLY] + ) + + _register_with_aliases(:irb_debug, Command::Debug, + [:debug, NO_OVERRIDE] + ) + _register_with_aliases(:irb_edit, Command::Edit, + [:edit, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_break, Command::Break) + _register_with_aliases(:irb_catch, Command::Catch) + _register_with_aliases(:irb_next, Command::Next) + _register_with_aliases(:irb_delete, Command::Delete, + [:delete, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_step, Command::Step, + [:step, NO_OVERRIDE] + ) + _register_with_aliases(:irb_continue, Command::Continue, + [:continue, NO_OVERRIDE] + ) + _register_with_aliases(:irb_finish, Command::Finish, + [:finish, NO_OVERRIDE] + ) + _register_with_aliases(:irb_backtrace, Command::Backtrace, + [:backtrace, NO_OVERRIDE], + [:bt, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_debug_info, Command::Info, + [:info, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_help, Command::Help, + [:help, NO_OVERRIDE], + [:show_cmds, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_show_doc, Command::ShowDoc, + [:show_doc, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_info, Command::IrbInfo) + + _register_with_aliases(:irb_ls, Command::Ls, + [:ls, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_measure, Command::Measure, + [:measure, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_show_source, Command::ShowSource, + [:show_source, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_whereami, Command::Whereami, + [:whereami, NO_OVERRIDE] + ) + + _register_with_aliases(:irb_history, Command::History, + [:history, NO_OVERRIDE], + [:hist, NO_OVERRIDE] + ) + end + + ExtendCommand = Command + + # For backward compatibility, we need to keep this module: + # - As a container of helper methods + # - As a place to register commands with the deprecated def_extend_command method + module ExtendCommandBundle + # For backward compatibility + NO_OVERRIDE = Command::NO_OVERRIDE + OVERRIDE_PRIVATE_ONLY = Command::OVERRIDE_PRIVATE_ONLY + OVERRIDE_ALL = Command::OVERRIDE_ALL + + # Deprecated. Doesn't have any effect. + @EXTEND_COMMANDS = [] + + # Drepcated. Use Command.regiser instead. + def self.def_extend_command(cmd_name, cmd_class, _, *aliases) + Command._register_with_aliases(cmd_name, cmd_class, *aliases) + Command.class_variable_set(:@@command_override_policies, nil) + end + end +end diff --git a/lib/irb/ext/change-ws.rb b/lib/irb/ext/change-ws.rb index c0f810a4c8..60e8afe31f 100644 --- a/lib/irb/ext/change-ws.rb +++ b/lib/irb/ext/change-ws.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/ext/cb.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -12,7 +12,7 @@ module IRB # :nodoc: if defined? @home_workspace @home_workspace else - @home_workspace = @workspace + @home_workspace = workspace end end @@ -25,15 +25,13 @@ module IRB # :nodoc: # See IRB::WorkSpace.new for more information. def change_workspace(*_main) if _main.empty? - @workspace = home_workspace + replace_workspace(home_workspace) return main end - @workspace = WorkSpace.new(_main[0]) - - if !(class<<main;ancestors;end).include?(ExtendCommandBundle) - main.extend ExtendCommandBundle - end + workspace = WorkSpace.new(_main[0]) + replace_workspace(workspace) + workspace.load_helper_methods_to_main end end end diff --git a/lib/irb/ext/eval_history.rb b/lib/irb/ext/eval_history.rb index 1a04178b40..6c21ff00ee 100644 --- a/lib/irb/ext/eval_history.rb +++ b/lib/irb/ext/eval_history.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # history.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -18,7 +18,7 @@ module IRB # :nodoc: if defined?(@eval_history) && @eval_history @eval_history_values.push @line_no, @last_value - @workspace.evaluate "__ = IRB.CurrentContext.instance_eval{@eval_history_values}" + workspace.evaluate "__ = IRB.CurrentContext.instance_eval{@eval_history_values}" end @last_value @@ -49,7 +49,7 @@ module IRB # :nodoc: else @eval_history_values = EvalHistory.new(no) IRB.conf[:__TMP__EHV__] = @eval_history_values - @workspace.evaluate("__ = IRB.conf[:__TMP__EHV__]") + workspace.evaluate("__ = IRB.conf[:__TMP__EHV__]") IRB.conf.delete(:__TMP_EHV__) end else diff --git a/lib/irb/ext/loader.rb b/lib/irb/ext/loader.rb index d65695df3b..df5aaa8e5a 100644 --- a/lib/irb/ext/loader.rb +++ b/lib/irb/ext/loader.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # loader.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -98,13 +98,13 @@ module IRB # :nodoc: def old # :nodoc: back_io = @io - back_path = @irb_path + back_path = irb_path back_name = @irb_name back_scanner = @irb.scanner begin @io = FileInputMethod.new(path) @irb_name = File.basename(path) - @irb_path = path + self.irb_path = path @irb.signal_status(:IN_LOAD) do if back_io.kind_of?(FileInputMethod) @irb.eval_input @@ -119,7 +119,7 @@ module IRB # :nodoc: ensure @io = back_io @irb_name = back_name - @irb_path = back_path + self.irb_path = back_path @irb.scanner = back_scanner end end diff --git a/lib/irb/ext/multi-irb.rb b/lib/irb/ext/multi-irb.rb index 1c20489137..9f234f0cdc 100644 --- a/lib/irb/ext/multi-irb.rb +++ b/lib/irb/ext/multi-irb.rb @@ -1,11 +1,11 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/multi-irb.rb - multiple irb module # by Keiju ISHITSUKA(keiju@ruby-lang.org) # module IRB - class JobManager + class JobManager # :nodoc: # Creates a new JobManager object def initialize @@ -166,12 +166,12 @@ module IRB @JobManager = JobManager.new # The current JobManager in the session - def IRB.JobManager + def IRB.JobManager # :nodoc: @JobManager end # The current Context in this session - def IRB.CurrentContext + def IRB.CurrentContext # :nodoc: IRB.JobManager.irb(Thread.current).context end @@ -179,7 +179,7 @@ module IRB # # The optional +file+ argument is given to Context.new, along with the # workspace created with the remaining arguments, see WorkSpace.new - def IRB.irb(file = nil, *main) + def IRB.irb(file = nil, *main) # :nodoc: workspace = WorkSpace.new(*main) parent_thread = Thread.current Thread.start do diff --git a/lib/irb/ext/tracer.rb b/lib/irb/ext/tracer.rb index 3eaeb70ef2..fd6daa88ae 100644 --- a/lib/irb/ext/tracer.rb +++ b/lib/irb/ext/tracer.rb @@ -1,78 +1,39 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/lib/tracer.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) # - +# Loading the gem "tracer" will cause it to extend IRB commands with: +# https://github.com/ruby/tracer/blob/v0.2.2/lib/tracer/irb.rb begin require "tracer" rescue LoadError $stderr.puts "Tracer extension of IRB is enabled but tracer gem wasn't found." - module IRB - class Context - def use_tracer=(opt) - # do nothing - end - end - end return # This is about to disable loading below end module IRB + class CallTracer < ::CallTracer + IRB_DIR = File.expand_path('../..', __dir__) - # initialize tracing function - def IRB.initialize_tracer - Tracer.verbose = false - Tracer.add_filter { - |event, file, line, id, binding, *rests| - /^#{Regexp.quote(@CONF[:IRB_LIB_PATH])}/ !~ file and - File::basename(file) != "irb.rb" - } - end - - class Context - # Whether Tracer is used when evaluating statements in this context. - # - # See +lib/tracer.rb+ for more information. - attr_reader :use_tracer - alias use_tracer? use_tracer - - # Sets whether or not to use the Tracer library when evaluating statements - # in this context. - # - # See +lib/tracer.rb+ for more information. - def use_tracer=(opt) - if opt - Tracer.set_get_line_procs(@irb_path) { - |line_no, *rests| - @io.line(line_no) - } - elsif !opt && @use_tracer - Tracer.off - end - @use_tracer=opt + def skip?(tp) + super || tp.path.match?(IRB_DIR) || tp.path.match?('<internal:prelude>') end end - class WorkSpace alias __evaluate__ evaluate # Evaluate the context of this workspace and use the Tracer library to # output the exact lines of code are being executed in chronological order. # - # See +lib/tracer.rb+ for more information. - def evaluate(context, statements, file = nil, line = nil) - if context.use_tracer? && file != nil && line != nil - Tracer.on - begin + # See https://github.com/ruby/tracer for more information. + def evaluate(statements, file = __FILE__, line = __LINE__) + if IRB.conf[:USE_TRACER] == true + CallTracer.new(colorize: Color.colorable?).start do __evaluate__(statements, file, line) - ensure - Tracer.off end else - __evaluate__(statements, file || __FILE__, line || __LINE__) + __evaluate__(statements, file, line) end end end - - IRB.initialize_tracer end diff --git a/lib/irb/ext/use-loader.rb b/lib/irb/ext/use-loader.rb index d0b8c2d4f4..c8a3ea1fe8 100644 --- a/lib/irb/ext/use-loader.rb +++ b/lib/irb/ext/use-loader.rb @@ -1,10 +1,10 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # use-loader.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) # -require_relative "../cmd/load" +require_relative "../command/load" require_relative "loader" class Object @@ -17,12 +17,12 @@ module IRB remove_method :irb_load if method_defined?(:irb_load) # Loads the given file similarly to Kernel#load, see IrbLoader#irb_load def irb_load(*opts, &b) - ExtendCommand::Load.execute(irb_context, *opts, &b) + Command::Load.execute(irb_context, *opts, &b) end remove_method :irb_require if method_defined?(:irb_require) # Loads the given file similarly to Kernel#require def irb_require(*opts, &b) - ExtendCommand::Require.execute(irb_context, *opts, &b) + Command::Require.execute(irb_context, *opts, &b) end end @@ -49,14 +49,12 @@ module IRB if IRB.conf[:USE_LOADER] != opt IRB.conf[:USE_LOADER] = opt if opt - if !$".include?("irb/cmd/load") - end - (class<<@workspace.main;self;end).instance_eval { + (class<<workspace.main;self;end).instance_eval { alias_method :load, :irb_load alias_method :require, :irb_require } else - (class<<@workspace.main;self;end).instance_eval { + (class<<workspace.main;self;end).instance_eval { alias_method :load, :__original__load__IRB_use_loader__ alias_method :require, :__original__require__IRB_use_loader__ } diff --git a/lib/irb/ext/workspaces.rb b/lib/irb/ext/workspaces.rb index 9defc3e17b..da09faa83e 100644 --- a/lib/irb/ext/workspaces.rb +++ b/lib/irb/ext/workspaces.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # push-ws.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -6,21 +6,6 @@ module IRB # :nodoc: class Context - - # Size of the current WorkSpace stack - def irb_level - workspace_stack.size - end - - # WorkSpaces in the current stack - def workspaces - if defined? @workspaces - @workspaces - else - @workspaces = [] - end - end - # Creates a new workspace with the given object or binding, and appends it # onto the current #workspaces stack. # @@ -28,20 +13,15 @@ module IRB # :nodoc: # information. def push_workspace(*_main) if _main.empty? - if workspaces.empty? - print "No other workspace\n" - return nil + if @workspace_stack.size > 1 + # swap the top two workspaces + previous_workspace, current_workspace = @workspace_stack.pop(2) + @workspace_stack.push current_workspace, previous_workspace end - ws = workspaces.pop - workspaces.push @workspace - @workspace = ws - return workspaces - end - - workspaces.push @workspace - @workspace = WorkSpace.new(@workspace.binding, _main[0]) - if !(class<<main;ancestors;end).include?(ExtendCommandBundle) - main.extend ExtendCommandBundle + else + new_workspace = WorkSpace.new(workspace.binding, _main[0]) + @workspace_stack.push new_workspace + new_workspace.load_helper_methods_to_main end end @@ -50,11 +30,7 @@ module IRB # :nodoc: # # Also, see #push_workspace. def pop_workspace - if workspaces.empty? - print "workspace stack empty\n" - return - end - @workspace = workspaces.pop + @workspace_stack.pop if @workspace_stack.size > 1 end end end diff --git a/lib/irb/extend-command.rb b/lib/irb/extend-command.rb deleted file mode 100644 index 072069d4c4..0000000000 --- a/lib/irb/extend-command.rb +++ /dev/null @@ -1,360 +0,0 @@ -# frozen_string_literal: false -# -# irb/extend-command.rb - irb extend command -# by Keiju ISHITSUKA(keiju@ruby-lang.org) -# - -module IRB # :nodoc: - # Installs the default irb extensions command bundle. - module ExtendCommandBundle - EXCB = ExtendCommandBundle # :nodoc: - - # See #install_alias_method. - NO_OVERRIDE = 0 - # See #install_alias_method. - OVERRIDE_PRIVATE_ONLY = 0x01 - # See #install_alias_method. - OVERRIDE_ALL = 0x02 - - # Quits the current irb context - # - # +ret+ is the optional signal or message to send to Context#exit - # - # Same as <code>IRB.CurrentContext.exit</code>. - def irb_exit(ret = 0) - irb_context.exit(ret) - end - - # Displays current configuration. - # - # Modifying the configuration is achieved by sending a message to IRB.conf. - def irb_context - IRB.CurrentContext - end - - @ALIASES = [ - [:context, :irb_context, NO_OVERRIDE], - [:conf, :irb_context, NO_OVERRIDE], - [:irb_quit, :irb_exit, OVERRIDE_PRIVATE_ONLY], - [:exit, :irb_exit, OVERRIDE_PRIVATE_ONLY], - [:quit, :irb_exit, OVERRIDE_PRIVATE_ONLY], - ] - - - @EXTEND_COMMANDS = [ - [ - :irb_current_working_workspace, :CurrentWorkingWorkspace, "cmd/chws", - [:cwws, NO_OVERRIDE], - [:pwws, NO_OVERRIDE], - [:irb_print_working_workspace, OVERRIDE_ALL], - [:irb_cwws, OVERRIDE_ALL], - [:irb_pwws, OVERRIDE_ALL], - [:irb_current_working_binding, OVERRIDE_ALL], - [:irb_print_working_binding, OVERRIDE_ALL], - [:irb_cwb, OVERRIDE_ALL], - [:irb_pwb, OVERRIDE_ALL], - ], - [ - :irb_change_workspace, :ChangeWorkspace, "cmd/chws", - [:chws, NO_OVERRIDE], - [:cws, NO_OVERRIDE], - [:irb_chws, OVERRIDE_ALL], - [:irb_cws, OVERRIDE_ALL], - [:irb_change_binding, OVERRIDE_ALL], - [:irb_cb, OVERRIDE_ALL], - [:cb, NO_OVERRIDE], - ], - - [ - :irb_workspaces, :Workspaces, "cmd/pushws", - [:workspaces, NO_OVERRIDE], - [:irb_bindings, OVERRIDE_ALL], - [:bindings, NO_OVERRIDE], - ], - [ - :irb_push_workspace, :PushWorkspace, "cmd/pushws", - [:pushws, NO_OVERRIDE], - [:irb_pushws, OVERRIDE_ALL], - [:irb_push_binding, OVERRIDE_ALL], - [:irb_pushb, OVERRIDE_ALL], - [:pushb, NO_OVERRIDE], - ], - [ - :irb_pop_workspace, :PopWorkspace, "cmd/pushws", - [:popws, NO_OVERRIDE], - [:irb_popws, OVERRIDE_ALL], - [:irb_pop_binding, OVERRIDE_ALL], - [:irb_popb, OVERRIDE_ALL], - [:popb, NO_OVERRIDE], - ], - - [ - :irb_load, :Load, "cmd/load"], - [ - :irb_require, :Require, "cmd/load"], - [ - :irb_source, :Source, "cmd/load", - [:source, NO_OVERRIDE], - ], - - [ - :irb, :IrbCommand, "cmd/subirb"], - [ - :irb_jobs, :Jobs, "cmd/subirb", - [:jobs, NO_OVERRIDE], - ], - [ - :irb_fg, :Foreground, "cmd/subirb", - [:fg, NO_OVERRIDE], - ], - [ - :irb_kill, :Kill, "cmd/subirb", - [:kill, OVERRIDE_PRIVATE_ONLY], - ], - - [ - :irb_debug, :Debug, "cmd/debug", - [:debug, NO_OVERRIDE], - ], - [ - :irb_edit, :Edit, "cmd/edit", - [:edit, NO_OVERRIDE], - ], - [ - :irb_break, :Break, "cmd/break", - ], - [ - :irb_catch, :Catch, "cmd/catch", - ], - [ - :irb_next, :Next, "cmd/next" - ], - [ - :irb_delete, :Delete, "cmd/delete", - [:delete, NO_OVERRIDE], - ], - [ - :irb_step, :Step, "cmd/step", - [:step, NO_OVERRIDE], - ], - [ - :irb_continue, :Continue, "cmd/continue", - [:continue, NO_OVERRIDE], - ], - [ - :irb_finish, :Finish, "cmd/finish", - [:finish, NO_OVERRIDE], - ], - [ - :irb_backtrace, :Backtrace, "cmd/backtrace", - [:backtrace, NO_OVERRIDE], - [:bt, NO_OVERRIDE], - ], - [ - :irb_debug_info, :Info, "cmd/info", - [:info, NO_OVERRIDE], - ], - - [ - :irb_help, :Help, "cmd/help", - [:help, NO_OVERRIDE], - ], - - [ - :irb_show_doc, :ShowDoc, "cmd/show_doc", - [:show_doc, NO_OVERRIDE], - ], - - [ - :irb_info, :IrbInfo, "cmd/irb_info" - ], - - [ - :irb_ls, :Ls, "cmd/ls", - [:ls, NO_OVERRIDE], - ], - - [ - :irb_measure, :Measure, "cmd/measure", - [:measure, NO_OVERRIDE], - ], - - [ - :irb_show_source, :ShowSource, "cmd/show_source", - [:show_source, NO_OVERRIDE], - ], - - [ - :irb_whereami, :Whereami, "cmd/whereami", - [:whereami, NO_OVERRIDE], - ], - [ - :irb_show_cmds, :ShowCmds, "cmd/show_cmds", - [:show_cmds, NO_OVERRIDE], - ], - - [ - :irb_history, :History, "cmd/history", - [:history, NO_OVERRIDE], - [:hist, NO_OVERRIDE], - ] - ] - - - @@commands = [] - - def self.all_commands_info - return @@commands unless @@commands.empty? - user_aliases = IRB.CurrentContext.command_aliases.each_with_object({}) do |(alias_name, target), result| - result[target] ||= [] - result[target] << alias_name - end - - @EXTEND_COMMANDS.each do |cmd_name, cmd_class, load_file, *aliases| - if !defined?(ExtendCommand) || !ExtendCommand.const_defined?(cmd_class, false) - require_relative load_file - end - - klass = ExtendCommand.const_get(cmd_class, false) - aliases = aliases.map { |a| a.first } - - if additional_aliases = user_aliases[cmd_name] - aliases += additional_aliases - end - - display_name = aliases.shift || cmd_name - @@commands << { display_name: display_name, description: klass.description, category: klass.category } - end - - @@commands - end - - # Convert a command name to its implementation class if such command exists - def self.load_command(command) - command = command.to_sym - @EXTEND_COMMANDS.each do |cmd_name, cmd_class, load_file, *aliases| - next if cmd_name != command && aliases.all? { |alias_name, _| alias_name != command } - - if !defined?(ExtendCommand) || !ExtendCommand.const_defined?(cmd_class, false) - require_relative load_file - end - return ExtendCommand.const_get(cmd_class, false) - end - nil - end - - # Installs the default irb commands. - def self.install_extend_commands - for args in @EXTEND_COMMANDS - def_extend_command(*args) - end - end - - # Evaluate the given +cmd_name+ on the given +cmd_class+ Class. - # - # Will also define any given +aliases+ for the method. - # - # The optional +load_file+ parameter will be required within the method - # definition. - def self.def_extend_command(cmd_name, cmd_class, load_file, *aliases) - case cmd_class - when Symbol - cmd_class = cmd_class.id2name - when String - when Class - cmd_class = cmd_class.name - end - - line = __LINE__; eval %[ - def #{cmd_name}(*opts, **kwargs, &b) - Kernel.require_relative "#{load_file}" - ::IRB::ExtendCommand::#{cmd_class}.execute(irb_context, *opts, **kwargs, &b) - end - ], nil, __FILE__, line - - for ali, flag in aliases - @ALIASES.push [ali, cmd_name, flag] - end - end - - # Installs alias methods for the default irb commands, see - # ::install_extend_commands. - def install_alias_method(to, from, override = NO_OVERRIDE) - to = to.id2name unless to.kind_of?(String) - from = from.id2name unless from.kind_of?(String) - - if override == OVERRIDE_ALL or - (override == OVERRIDE_PRIVATE_ONLY) && !respond_to?(to) or - (override == NO_OVERRIDE) && !respond_to?(to, true) - target = self - (class << self; self; end).instance_eval{ - if target.respond_to?(to, true) && - !target.respond_to?(EXCB.irb_original_method_name(to), true) - alias_method(EXCB.irb_original_method_name(to), to) - end - alias_method to, from - } - else - Kernel.warn "irb: warn: can't alias #{to} from #{from}.\n" - end - end - - def self.irb_original_method_name(method_name) # :nodoc: - "irb_" + method_name + "_org" - end - - # Installs alias methods for the default irb commands on the given object - # using #install_alias_method. - def self.extend_object(obj) - unless (class << obj; ancestors; end).include?(EXCB) - super - for ali, com, flg in @ALIASES - obj.install_alias_method(ali, com, flg) - end - end - end - - install_extend_commands - end - - # Extends methods for the Context module - module ContextExtender - CE = ContextExtender # :nodoc: - - @EXTEND_COMMANDS = [ - [:eval_history=, "ext/eval_history.rb"], - [:use_tracer=, "ext/tracer.rb"], - [:use_loader=, "ext/use-loader.rb"], - ] - - # Installs the default context extensions as irb commands: - # - # Context#eval_history=:: +irb/ext/history.rb+ - # Context#use_tracer=:: +irb/ext/tracer.rb+ - # Context#use_loader=:: +irb/ext/use-loader.rb+ - def self.install_extend_commands - for args in @EXTEND_COMMANDS - def_extend_command(*args) - end - end - - # Evaluate the given +command+ from the given +load_file+ on the Context - # module. - # - # Will also define any given +aliases+ for the method. - def self.def_extend_command(cmd_name, load_file, *aliases) - line = __LINE__; Context.module_eval %[ - def #{cmd_name}(*opts, &b) - Context.module_eval {remove_method(:#{cmd_name})} - require_relative "#{load_file}" - __send__ :#{cmd_name}, *opts, &b - end - for ali in aliases - alias_method ali, cmd_name - end - ], __FILE__, line - end - - CE.install_extend_commands - end -end diff --git a/lib/irb/frame.rb b/lib/irb/frame.rb index 14768bd8f6..4b697c8719 100644 --- a/lib/irb/frame.rb +++ b/lib/irb/frame.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # frame.rb - # by Keiju ISHITSUKA(Nihon Rational Software Co.,Ltd) diff --git a/lib/irb/help.rb b/lib/irb/help.rb index 6861d7efc8..a24bc10a15 100644 --- a/lib/irb/help.rb +++ b/lib/irb/help.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/help.rb - print usage module # by Keiju ISHITSUKA(keiju@ishitsuka.com) @@ -6,7 +6,7 @@ module IRB # Outputs the irb help message, see IRB@Command-Line+Options. - def IRB.print_usage + def IRB.print_usage # :nodoc: lc = IRB.conf[:LC_MESSAGES] path = lc.find("irb/help-message") space_line = false diff --git a/lib/irb/helper_method.rb b/lib/irb/helper_method.rb new file mode 100644 index 0000000000..f1f6fff915 --- /dev/null +++ b/lib/irb/helper_method.rb @@ -0,0 +1,29 @@ +require_relative "helper_method/base" + +module IRB + module HelperMethod + @helper_methods = {} + + class << self + attr_reader :helper_methods + + def register(name, helper_class) + @helper_methods[name] = helper_class + + if defined?(HelpersContainer) + HelpersContainer.install_helper_methods + end + end + + def all_helper_methods_info + @helper_methods.map do |name, helper_class| + { display_name: name, description: helper_class.description } + end + end + end + + # Default helper_methods + require_relative "helper_method/conf" + register(:conf, HelperMethod::Conf) + end +end diff --git a/lib/irb/helper_method/base.rb b/lib/irb/helper_method/base.rb new file mode 100644 index 0000000000..a68001ed28 --- /dev/null +++ b/lib/irb/helper_method/base.rb @@ -0,0 +1,16 @@ +require "singleton" + +module IRB + module HelperMethod + class Base + include Singleton + + class << self + def description(description = nil) + @description = description if description + @description + end + end + end + end +end diff --git a/lib/irb/helper_method/conf.rb b/lib/irb/helper_method/conf.rb new file mode 100644 index 0000000000..718ed279c0 --- /dev/null +++ b/lib/irb/helper_method/conf.rb @@ -0,0 +1,11 @@ +module IRB + module HelperMethod + class Conf < Base + description "Returns the current IRB context." + + def execute + IRB.CurrentContext + end + end + end +end diff --git a/lib/irb/history.rb b/lib/irb/history.rb index 06088adb0d..685354b2d8 100644 --- a/lib/irb/history.rb +++ b/lib/irb/history.rb @@ -1,3 +1,5 @@ +require "pathname" + module IRB module HistorySavingAbility # :nodoc: def support_history_saving? @@ -5,7 +7,7 @@ module IRB end def reset_history_counter - @loaded_history_lines = self.class::HISTORY.size if defined? @loaded_history_lines + @loaded_history_lines = self.class::HISTORY.size end def load_history @@ -15,7 +17,7 @@ module IRB history_file = File.expand_path(history_file) end history_file = IRB.rc_file("_history") unless history_file - if File.exist?(history_file) + if history_file && File.exist?(history_file) File.open(history_file, "r:#{IRB.conf[:LC_MESSAGES].encoding}") do |f| f.each { |l| l = l.chomp @@ -41,6 +43,9 @@ module IRB end history_file = IRB.rc_file("_history") unless history_file + # When HOME and XDG_CONFIG_HOME are not available, history_file might be nil + return unless history_file + # Change the permission of a file that already exists[BUG #7694] begin if File.stat(history_file).mode & 066 != 0 @@ -59,13 +64,19 @@ module IRB append_history = true end + pathname = Pathname.new(history_file) + unless Dir.exist?(pathname.dirname) + warn "Warning: The directory to save IRB's history file does not exist. Please double check `IRB.conf[:HISTORY_FILE]`'s value." + return + end + File.open(history_file, (append_history ? 'a' : 'w'), 0o600, encoding: IRB.conf[:LC_MESSAGES]&.encoding) do |f| hist = history.map{ |l| l.scrub.split("\n").join("\\\n") } unless append_history begin hist = hist.last(num) if hist.size > num and num > 0 rescue RangeError # bignum too big to convert into `long' - # Do nothing because the bignum should be treated as inifinity + # Do nothing because the bignum should be treated as infinity end end f.puts(hist) diff --git a/lib/irb/init.rb b/lib/irb/init.rb index 66e7b61468..355047519c 100644 --- a/lib/irb/init.rb +++ b/lib/irb/init.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/init.rb - irb initialize module # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -6,6 +6,7 @@ module IRB # :nodoc: @CONF = {} + @INITIALIZED = false # Displays current configuration. # # Modifying the configuration is achieved by sending a message to IRB.conf. @@ -41,6 +42,10 @@ module IRB # :nodoc: format("irb %s (%s)", @RELEASE_VERSION, @LAST_UPDATE_DATE) end + def IRB.initialized? + !!@INITIALIZED + end + # initialize config def IRB.setup(ap_path, argv: ::ARGV) IRB.init_config(ap_path) @@ -52,13 +57,11 @@ module IRB # :nodoc: unless @CONF[:PROMPT][@CONF[:PROMPT_MODE]] fail UndefinedPromptMode, @CONF[:PROMPT_MODE] end + @INITIALIZED = true end # @CONF default setting def IRB.init_config(ap_path) - # class instance variables - @TRACER_INITIALIZED = false - # default configurations unless ap_path and @CONF[:AP_NAME] ap_path = File.join(File.dirname(File.dirname(__FILE__)), "irb.rb") @@ -392,56 +395,36 @@ module IRB # :nodoc: # Run the config file def IRB.run_config if @CONF[:RC] - begin - file = rc_file - # Because rc_file always returns `HOME/.irbrc` even if no rc file is present, we can't warn users about missing rc files. - # Otherwise, it'd be very noisy. - load file if File.exist?(file) + irbrc_files.each do |rc| + load rc rescue StandardError, ScriptError => e - warn "Error loading RC file '#{file}':\n#{e.full_message(highlight: false)}" + warn "Error loading RC file '#{rc}':\n#{e.full_message(highlight: false)}" end end end IRBRC_EXT = "rc" - def IRB.rc_file(ext = IRBRC_EXT) - if !@CONF[:RC_NAME_GENERATOR] - rc_file_generators do |rcgen| - @CONF[:RC_NAME_GENERATOR] ||= rcgen - if File.exist?(rcgen.call(IRBRC_EXT)) - @CONF[:RC_NAME_GENERATOR] = rcgen - break - end - end + + def IRB.rc_file(ext) + prepare_irbrc_name_generators + + # When irbrc exist in default location + if (rcgen = @existing_rc_name_generators.first) + return rcgen.call(ext) end - case rc_file = @CONF[:RC_NAME_GENERATOR].call(ext) - when String - rc_file - else - fail IllegalRCNameGenerator + + # When irbrc does not exist in default location + rc_file_generators do |rcgen| + return rcgen.call(ext) end + + # When HOME and XDG_CONFIG_HOME are not available + nil end - # enumerate possible rc-file base name generators - def IRB.rc_file_generators - if irbrc = ENV["IRBRC"] - yield proc{|rc| rc == "rc" ? irbrc : irbrc+rc} - end - if xdg_config_home = ENV["XDG_CONFIG_HOME"] - irb_home = File.join(xdg_config_home, "irb") - if File.directory?(irb_home) - yield proc{|rc| irb_home + "/irb#{rc}"} - end - end - if home = ENV["HOME"] - yield proc{|rc| home+"/.irb#{rc}"} - yield proc{|rc| home+"/.config/irb/irb#{rc}"} - end - current_dir = Dir.pwd - yield proc{|rc| current_dir+"/.irb#{rc}"} - yield proc{|rc| current_dir+"/irb#{rc.sub(/\A_?/, '.')}"} - yield proc{|rc| current_dir+"/_irb#{rc}"} - yield proc{|rc| current_dir+"/$irb#{rc}"} + def IRB.irbrc_files + prepare_irbrc_name_generators + @irbrc_files end # loading modules @@ -457,6 +440,50 @@ module IRB # :nodoc: class << IRB private + + def prepare_irbrc_name_generators + return if @existing_rc_name_generators + + @existing_rc_name_generators = [] + @irbrc_files = [] + rc_file_generators do |rcgen| + irbrc = rcgen.call(IRBRC_EXT) + if File.exist?(irbrc) + @irbrc_files << irbrc + @existing_rc_name_generators << rcgen + end + end + generate_current_dir_irbrc_files.each do |irbrc| + @irbrc_files << irbrc if File.exist?(irbrc) + end + @irbrc_files.uniq! + end + + # enumerate possible rc-file base name generators + def rc_file_generators + if irbrc = ENV["IRBRC"] + yield proc{|rc| rc == "rc" ? irbrc : irbrc+rc} + end + if xdg_config_home = ENV["XDG_CONFIG_HOME"] + irb_home = File.join(xdg_config_home, "irb") + if File.directory?(irb_home) + yield proc{|rc| irb_home + "/irb#{rc}"} + end + end + if home = ENV["HOME"] + yield proc{|rc| home+"/.irb#{rc}"} + if xdg_config_home.nil? || xdg_config_home.empty? + yield proc{|rc| home+"/.config/irb/irb#{rc}"} + end + end + end + + # possible irbrc files in current directory + def generate_current_dir_irbrc_files + current_dir = Dir.pwd + %w[.irbrc irbrc _irbrc $irbrc].map { |file| "#{current_dir}/#{file}" } + end + def set_encoding(extern, intern = nil, override: true) verbose, $VERBOSE = $VERBOSE, nil Encoding.default_external = extern unless extern.nil? || extern.empty? diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index b74974b925..684527edc4 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/input-method.rb - input methods used irb # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -20,7 +20,7 @@ module IRB # # See IO#gets for more information. def gets - fail NotImplementedError, "gets" + fail NotImplementedError end public :gets @@ -44,6 +44,10 @@ module IRB false end + def prompting? + false + end + # For debug message def inspect 'Abstract InputMethod' @@ -63,6 +67,7 @@ module IRB # # See IO#gets for more information. def gets + puts if @stdout.tty? # workaround for debug compatibility test print @prompt line = @stdin.gets @line[@line_no += 1] = line @@ -91,6 +96,10 @@ module IRB true end + def prompting? + STDIN.tty? + end + # Returns the current line number for #io. # # #line counts the number of times #gets is called. @@ -220,6 +229,10 @@ module IRB @eof end + def prompting? + true + end + # For debug message def inspect readline_impl = (defined?(Reline) && Readline == Reline) ? 'Reline' : 'ext/readline' @@ -291,11 +304,27 @@ module IRB @auto_indent_proc = block end + def retrieve_doc_namespace(matched) + preposing, _target, postposing, bind = @completion_params + @completor.doc_namespace(preposing, matched, postposing, bind: bind) + end + + def rdoc_ri_driver + return @rdoc_ri_driver if defined?(@rdoc_ri_driver) + + begin + require 'rdoc' + rescue LoadError + @rdoc_ri_driver = nil + else + options = {} + options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty? + @rdoc_ri_driver = RDoc::RI::Driver.new(options) + end + end + def show_doc_dialog_proc - doc_namespace = ->(matched) { - preposing, _target, postposing, bind = @completion_params - @completor.doc_namespace(preposing, matched, postposing, bind: bind) - } + input_method = self # self is changed in the lambda below. ->() { dialog.trap_key = nil alt_d = [ @@ -311,15 +340,13 @@ module IRB cursor_pos_to_render, result, pointer, autocomplete_dialog = context.pop(4) return nil if result.nil? or pointer.nil? or pointer < 0 - name = doc_namespace.call(result[pointer]) + name = input_method.retrieve_doc_namespace(result[pointer]) # Use first one because document dialog does not support multiple namespaces. name = name.first if name.is_a?(Array) show_easter_egg = name&.match?(/\ARubyVM/) && !ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER'] - options = {} - options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty? - driver = RDoc::RI::Driver.new(options) + driver = input_method.rdoc_ri_driver if key.match?(dialog.name) if show_easter_egg @@ -407,23 +434,18 @@ module IRB } end - def display_document(matched, driver: nil) - begin - require 'rdoc' - rescue LoadError - return - end + def display_document(matched) + driver = rdoc_ri_driver + return unless driver if matched =~ /\A(?:::)?RubyVM/ and not ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER'] IRB.__send__(:easter_egg) return end - _target, preposing, postposing, bind = @completion_params - namespace = @completor.doc_namespace(preposing, matched, postposing, bind: bind) + namespace = retrieve_doc_namespace(matched) return unless namespace - driver ||= RDoc::RI::Driver.new if namespace.is_a?(Array) out = RDoc::Markup::Document.new namespace.each do |m| @@ -466,6 +488,10 @@ module IRB @eof end + def prompting? + true + end + # For debug message def inspect config = Reline::Config.new diff --git a/lib/irb/inspector.rb b/lib/irb/inspector.rb index ee3b19efdc..667087ccba 100644 --- a/lib/irb/inspector.rb +++ b/lib/irb/inspector.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/inspector.rb - inspect methods # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -46,7 +46,7 @@ module IRB # :nodoc: # Determines the inspector to use where +inspector+ is one of the keys passed # during inspector definition. def self.keys_with_inspector(inspector) - INSPECTORS.select{|k,v| v == inspector}.collect{|k, v| k} + INSPECTORS.select{|k, v| v == inspector}.collect{|k, v| k} end # Example @@ -113,7 +113,7 @@ module IRB # :nodoc: Color.colorize_code(v.inspect, colorable: Color.colorable? && Color.inspect_colorable?(v)) } Inspector.def_inspector([true, :pp, :pretty_inspect], proc{require_relative "color_printer"}){|v| - IRB::ColorPrinter.pp(v, '').chomp + IRB::ColorPrinter.pp(v, +'').chomp } Inspector.def_inspector([:yaml, :YAML], proc{require "yaml"}){|v| begin diff --git a/lib/irb/irb.gemspec b/lib/irb/irb.gemspec index a008a39f9d..b29002f593 100644 --- a/lib/irb/irb.gemspec +++ b/lib/irb/irb.gemspec @@ -41,6 +41,6 @@ Gem::Specification.new do |spec| spec.required_ruby_version = Gem::Requirement.new(">= 2.7") - spec.add_dependency "reline", ">= 0.3.8" - spec.add_dependency "rdoc" + spec.add_dependency "reline", ">= 0.4.2" + spec.add_dependency "rdoc", ">= 4.0.0" end diff --git a/lib/irb/lc/error.rb b/lib/irb/lc/error.rb index a5ec150865..ee0f047822 100644 --- a/lib/irb/lc/error.rb +++ b/lib/irb/lc/error.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/lc/error.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -12,11 +12,6 @@ module IRB super("Unrecognized switch: #{val}") end end - class NotImplementedError < StandardError - def initialize(val) - super("Need to define `#{val}'") - end - end class CantReturnToNormalMode < StandardError def initialize super("Can't return to normal mode.") @@ -52,11 +47,6 @@ module IRB super("Undefined prompt mode(#{val}).") end end - class IllegalRCGenerator < StandardError - def initialize - super("Define illegal RC_NAME_GENERATOR.") - end - end # :startdoc: end diff --git a/lib/irb/lc/ja/error.rb b/lib/irb/lc/ja/error.rb index 50d72c4a10..9e2e5b8870 100644 --- a/lib/irb/lc/ja/error.rb +++ b/lib/irb/lc/ja/error.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/lc/ja/error.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -12,11 +12,6 @@ module IRB super("スイッチ(#{val})が分りません") end end - class NotImplementedError < StandardError - def initialize(val) - super("`#{val}'の定義が必要です") - end - end class CantReturnToNormalMode < StandardError def initialize super("Normalモードに戻れません.") @@ -52,11 +47,6 @@ module IRB super("プロンプトモード(#{val})は定義されていません.") end end - class IllegalRCGenerator < StandardError - def initialize - super("RC_NAME_GENERATORが正しく定義されていません.") - end - end # :startdoc: end diff --git a/lib/irb/lc/ja/help-message b/lib/irb/lc/ja/help-message index cec339cf2f..99f4449b3b 100644 --- a/lib/irb/lc/ja/help-message +++ b/lib/irb/lc/ja/help-message @@ -9,10 +9,18 @@ Usage: irb.rb [options] [programfile] [arguments] -W[level=2] ruby -W と同じ. --context-mode n 新しいワークスペースを作成した時に関連する Binding オブジェクトの作成方法を 0 から 3 のいずれかに設定する. + --extra-doc-dir 指定したディレクトリのドキュメントを追加で読み込む. --echo 実行結果を表示する(デフォルト). --noecho 実行結果を表示しない. + --echo-on-assignment + 代入結果を表示する. + --noecho-on-assignment + 代入結果を表示しない. + --truncate-echo-on-assignment + truncateされた代入結果を表示する(デフォルト). --inspect 結果出力にinspectを用いる. --noinspect 結果出力にinspectを用いない. + --no-pager ページャを使用しない. --multiline マルチラインエディタを利用する. --nomultiline マルチラインエディタを利用しない. --singleline シングルラインエディタを利用する. @@ -34,6 +42,8 @@ Usage: irb.rb [options] [programfile] [arguments] --sample-book-mode/--simple-prompt 非常にシンプルなプロンプトを用いるモードです. --noprompt プロンプト表示を行なわない. + --script スクリプトモード(最初の引数をスクリプトファイルとして扱う、デフォルト) + --noscript 引数をargvとして扱う. --single-irb irb 中で self を実行して得られるオブジェクトをサ ブ irb と共有する. --tracer コマンド実行時にトレースを行なう. diff --git a/lib/irb/locale.rb b/lib/irb/locale.rb index f94aa0f40b..2abcc7354b 100644 --- a/lib/irb/locale.rb +++ b/lib/irb/locale.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/locale.rb - internationalization module # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -94,7 +94,7 @@ module IRB # :nodoc: end end - def find(file , paths = $:) + def find(file, paths = $:) dir = File.dirname(file) dir = "" if dir == "." base = File.basename(file) diff --git a/lib/irb/nesting_parser.rb b/lib/irb/nesting_parser.rb index 3d4db82444..5aa940cc28 100644 --- a/lib/irb/nesting_parser.rb +++ b/lib/irb/nesting_parser.rb @@ -12,6 +12,8 @@ module IRB skip = false last_tok, state, args = opens.last case state + when :in_alias_undef + skip = t.event == :on_kw when :in_unquoted_symbol unless IGNORE_TOKENS.include?(t.event) opens.pop @@ -61,17 +63,17 @@ module IRB if args.include?(:arg) case t.event when :on_nl, :on_semicolon - # def recever.f; + # def receiver.f; body = :normal when :on_lparen - # def recever.f() + # def receiver.f() next_args << :eq else if t.event == :on_op && t.tok == '=' # def receiver.f = body = :oneliner else - # def recever.f arg + # def receiver.f arg next_args << :arg_without_paren end end @@ -130,6 +132,10 @@ module IRB opens.pop opens << [t, nil] end + when 'alias' + opens << [t, :in_alias_undef, 2] + when 'undef' + opens << [t, :in_alias_undef, 1] when 'elsif', 'else', 'when' opens.pop opens << [t, nil] @@ -174,6 +180,10 @@ module IRB pending_heredocs.reverse_each { |t| opens << [t, nil] } pending_heredocs = [] end + if opens.last && opens.last[1] == :in_alias_undef && !IGNORE_TOKENS.include?(t.event) && t.event != :on_heredoc_end + tok, state, arg = opens.pop + opens << [tok, state, arg - 1] if arg >= 1 + end yield t, opens if block_given? end opens.map(&:first) + pending_heredocs.reverse diff --git a/lib/irb/notifier.rb b/lib/irb/notifier.rb index 612de3df16..dc1b9ef14b 100644 --- a/lib/irb/notifier.rb +++ b/lib/irb/notifier.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # notifier.rb - output methods used by irb # by Keiju ISHITSUKA(keiju@ruby-lang.org) diff --git a/lib/irb/output-method.rb b/lib/irb/output-method.rb index f5ea57111d..69942f47a2 100644 --- a/lib/irb/output-method.rb +++ b/lib/irb/output-method.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # output-method.rb - output methods used by irb # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -9,16 +9,10 @@ module IRB # IRB::Notifier. You can define your own output method to use with Irb.new, # or Context.new class OutputMethod - class NotImplementedError < StandardError - def initialize(val) - super("Need to define `#{val}'") - end - end - # Open this method to implement your own output method, raises a # NotImplementedError if you don't define #print in your own class. def print(*opts) - raise NotImplementedError, "print" + raise NotImplementedError end # Prints the given +opts+, with a newline delimiter. diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index 4bce2aa6b2..cfe36be83f 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/ruby-lex.rb - ruby lexcal analyzer # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -290,7 +290,7 @@ module IRB when :on_embdoc_beg indent_level = 0 else - indent_level += 1 + indent_level += 1 unless t.tok == 'alias' || t.tok == 'undef' end end indent_level diff --git a/lib/irb/source_finder.rb b/lib/irb/source_finder.rb index 659d4200fd..5d7d729d19 100644 --- a/lib/irb/source_finder.rb +++ b/lib/irb/source_finder.rb @@ -4,12 +4,63 @@ require_relative "ruby-lex" module IRB class SourceFinder - Source = Struct.new( - :file, # @param [String] - file name - :first_line, # @param [String] - first line - :last_line, # @param [String] - last line - keyword_init: true, - ) + class EvaluationError < StandardError; end + + class Source + attr_reader :file, :line + def initialize(file, line, ast_source = nil) + @file = file + @line = line + @ast_source = ast_source + end + + def file_exist? + File.exist?(@file) + end + + def binary_file? + # If the line is zero, it means that the target's source is probably in a binary file. + @line.zero? + end + + def file_content + @file_content ||= File.read(@file) + end + + def colorized_content + if !binary_file? && file_exist? + end_line = find_end + # To correctly colorize, we need to colorize full content and extract the relevant lines. + colored = IRB::Color.colorize_code(file_content) + colored.lines[@line - 1...end_line].join + elsif @ast_source + IRB::Color.colorize_code(@ast_source) + end + end + + private + + def find_end + lex = RubyLex.new + code = file_content + lines = code.lines[(@line - 1)..-1] + tokens = RubyLex.ripper_lex_without_warning(lines.join) + prev_tokens = [] + + # chunk with line number + tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk| + code = lines[0..lnum].join + prev_tokens.concat chunk + continue = lex.should_continue?(prev_tokens) + syntax = lex.check_code_syntax(code, local_variables: []) + if !continue && syntax == :valid + return @line + lnum + end + end + @line + end + end + private_constant :Source def initialize(irb_context) @@ -17,49 +68,47 @@ module IRB end def find_source(signature, super_level = 0) - context_binding = @irb_context.workspace.binding case signature - when /\A[A-Z]\w*(::[A-Z]\w*)*\z/ # Const::Name - eval(signature, context_binding) # trigger autoload - base = context_binding.receiver.yield_self { |r| r.is_a?(Module) ? r : Object } - file, line = base.const_source_location(signature) + when /\A(::)?[A-Z]\w*(::[A-Z]\w*)*\z/ # ConstName, ::ConstName, ConstPath::ConstName + eval_receiver_or_owner(signature) # trigger autoload + *parts, name = signature.split('::', -1) + base = + if parts.empty? # ConstName + find_const_owner(name) + elsif parts == [''] # ::ConstName + Object + else # ConstPath::ConstName + eval_receiver_or_owner(parts.join('::')) + end + file, line = base.const_source_location(name) when /\A(?<owner>[A-Z]\w*(::[A-Z]\w*)*)#(?<method>[^ :.]+)\z/ # Class#method - owner = eval(Regexp.last_match[:owner], context_binding) + owner = eval_receiver_or_owner(Regexp.last_match[:owner]) method = Regexp.last_match[:method] return unless owner.respond_to?(:instance_method) - file, line = method_target(owner, super_level, method, "owner") + method = method_target(owner, super_level, method, "owner") + file, line = method&.source_location when /\A((?<receiver>.+)(\.|::))?(?<method>[^ :.]+)\z/ # method, receiver.method, receiver::method - receiver = eval(Regexp.last_match[:receiver] || 'self', context_binding) + receiver = eval_receiver_or_owner(Regexp.last_match[:receiver] || 'self') method = Regexp.last_match[:method] return unless receiver.respond_to?(method, true) - file, line = method_target(receiver, super_level, method, "receiver") + method = method_target(receiver, super_level, method, "receiver") + file, line = method&.source_location end - if file && line && File.exist?(file) - Source.new(file: file, first_line: line, last_line: find_end(file, line)) + return unless file && line + + if File.exist?(file) + Source.new(file, line) + elsif method + # Method defined with eval, probably in IRB session + source = RubyVM::AbstractSyntaxTree.of(method)&.source rescue nil + Source.new(file, line, source) end + rescue EvaluationError + nil end private - def find_end(file, first_line) - lex = RubyLex.new - lines = File.read(file).lines[(first_line - 1)..-1] - tokens = RubyLex.ripper_lex_without_warning(lines.join) - prev_tokens = [] - - # chunk with line number - tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk| - code = lines[0..lnum].join - prev_tokens.concat chunk - continue = lex.should_continue?(prev_tokens) - syntax = lex.check_code_syntax(code, local_variables: []) - if !continue && syntax == :valid - return first_line + lnum - end - end - first_line - end - def method_target(owner_receiver, super_level, method, type) case type when "owner" @@ -70,9 +119,21 @@ module IRB super_level.times do |s| target_method = target_method.super_method if target_method end - target_method.nil? ? nil : target_method.source_location + target_method rescue NameError nil end + + def eval_receiver_or_owner(code) + context_binding = @irb_context.workspace.binding + eval(code, context_binding) + rescue NameError + raise EvaluationError + end + + def find_const_owner(name) + module_nesting = @irb_context.workspace.binding.eval('::Module.nesting') + module_nesting.find { |mod| mod.const_defined?(name, false) } || module_nesting.find { |mod| mod.const_defined?(name) } || Object + end end end diff --git a/lib/irb/statement.rb b/lib/irb/statement.rb index b12110600c..a3391c12a3 100644 --- a/lib/irb/statement.rb +++ b/lib/irb/statement.rb @@ -16,8 +16,23 @@ module IRB raise NotImplementedError end - def evaluable_code - raise NotImplementedError + class EmptyInput < Statement + def is_assignment? + false + end + + def suppresses_echo? + true + end + + # Debugger takes empty input to repeat the last command + def should_be_handled_by_debugger? + true + end + + def code + "" + end end class Expression < Statement @@ -37,18 +52,15 @@ module IRB def is_assignment? @is_assignment end - - def evaluable_code - @code - end end class Command < Statement - def initialize(code, command, arg, command_class) - @code = code - @command = command - @arg = arg + attr_reader :command_class, :arg + + def initialize(original_code, command_class, arg) + @code = original_code @command_class = command_class + @arg = arg end def is_assignment? @@ -60,20 +72,8 @@ module IRB end def should_be_handled_by_debugger? - require_relative 'cmd/help' - require_relative 'cmd/debug' - IRB::ExtendCommand::DebugCommand > @command_class || IRB::ExtendCommand::Help == @command_class - end - - def evaluable_code - # Hook command-specific transformation to return valid Ruby code - if @command_class.respond_to?(:transform_args) - arg = @command_class.transform_args(@arg) - else - arg = @arg - end - - [@command, arg].compact.join(' ') + require_relative 'command/debug' + IRB::Command::DebugCommand > @command_class end end end diff --git a/lib/irb/version.rb b/lib/irb/version.rb index c0be6d91e5..c41917329c 100644 --- a/lib/irb/version.rb +++ b/lib/irb/version.rb @@ -1,11 +1,11 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/version.rb - irb version definition file # by Keiju ISHITSUKA(keiju@ishitsuka.com) # module IRB # :nodoc: - VERSION = "1.11.0" + VERSION = "1.13.1" @RELEASE_VERSION = VERSION - @LAST_UPDATE_DATE = "2023-12-19" + @LAST_UPDATE_DATE = "2024-05-05" end diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb index 2bf3d5e0f1..d24d1cc38d 100644 --- a/lib/irb/workspace.rb +++ b/lib/irb/workspace.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/workspace-binding.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) @@ -6,6 +6,8 @@ require "delegate" +require_relative "helper_method" + IRB::TOPLEVEL_BINDING = binding module IRB # :nodoc: class WorkSpace @@ -90,11 +92,11 @@ EOF IRB.conf[:__MAIN__] = @main @main.singleton_class.class_eval do private - define_method(:exit) do |*a, &b| - # Do nothing, will be overridden - end define_method(:binding, Kernel.instance_method(:binding)) define_method(:local_variables, Kernel.instance_method(:local_variables)) + # Define empty method to avoid delegator warning, will be overridden. + define_method(:exit) {|*a, &b| } + define_method(:exit!) {|*a, &b| } end @binding = eval("IRB.conf[:__MAIN__].instance_eval('binding', __FILE__, __LINE__)", @binding, *@binding.source_location) end @@ -108,8 +110,10 @@ EOF # <code>IRB.conf[:__MAIN__]</code> attr_reader :main - def load_commands_to_main - main.extend ExtendCommandBundle + def load_helper_methods_to_main + ancestors = class<<main;ancestors;end + main.extend ExtendCommandBundle if !ancestors.include?(ExtendCommandBundle) + main.extend HelpersContainer if !ancestors.include?(HelpersContainer) end # Evaluate the given +statements+ within the context of this workspace. @@ -170,4 +174,16 @@ EOF "\nFrom: #{file} @ line #{pos + 1} :\n\n#{body}#{Color.clear}\n" end end + + module HelpersContainer + def self.install_helper_methods + HelperMethod.helper_methods.each do |name, helper_method_class| + define_method name do |*args, **opts, &block| + helper_method_class.instance.execute(*args, **opts, &block) + end unless method_defined?(name) + end + end + + install_helper_methods + end end diff --git a/lib/irb/ws-for-case-2.rb b/lib/irb/ws-for-case-2.rb index a0f617e4ed..03f42d73d9 100644 --- a/lib/irb/ws-for-case-2.rb +++ b/lib/irb/ws-for-case-2.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/ws-for-case-2.rb - # by Keiju ISHITSUKA(keiju@ruby-lang.org) diff --git a/lib/irb/xmp.rb b/lib/irb/xmp.rb index de29089429..b1bc53283e 100644 --- a/lib/irb/xmp.rb +++ b/lib/irb/xmp.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # xmp.rb - irb version of gotoken xmp # by Keiju ISHITSUKA(Nippon Rational Inc.) diff --git a/lib/reline.rb b/lib/reline.rb index fb19982081..fb00b96531 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -75,10 +75,10 @@ module Reline def initialize self.output = STDOUT + @mutex = Mutex.new @dialog_proc_list = {} yield self @completion_quote_character = nil - @bracketed_paste_finished = false end def io_gate @@ -220,32 +220,25 @@ module Reline Reline::DEFAULT_DIALOG_PROC_AUTOCOMPLETE = ->() { # autocomplete - return nil unless config.autocompletion - if just_cursor_moving and completion_journey_data.nil? - # Auto complete starts only when edited - return nil - end - pre, target, post = retrieve_completion_block(true) - if target.nil? or target.empty? or (completion_journey_data&.pointer == -1 and target.size <= 3) - return nil - end - if completion_journey_data and completion_journey_data.list - result = completion_journey_data.list.dup - result.shift - pointer = completion_journey_data.pointer - 1 - else - result = call_completion_proc_with_checking_args(pre, target, post) - pointer = nil - end - if result and result.size == 1 and result[0] == target and pointer != 0 - result = nil - end + return unless config.autocompletion + + journey_data = completion_journey_data + return unless journey_data + + target = journey_data.list.first + completed = journey_data.list[journey_data.pointer] + result = journey_data.list.drop(1) + pointer = journey_data.pointer - 1 + return if completed.empty? || (result == [completed] && pointer < 0) + target_width = Reline::Unicode.calculate_width(target) - x = cursor_pos.x - target_width - if x < 0 - x = screen_width + x + completed_width = Reline::Unicode.calculate_width(completed) + if cursor_pos.x <= completed_width - target_width + # When target is rendered on the line above cursor position + x = screen_width - completed_width y = -1 else + x = [cursor_pos.x - completed_width, 0].max y = 0 end cursor_pos_to_render = Reline::CursorPos.new(x, y) @@ -265,12 +258,15 @@ module Reline Reline::DEFAULT_DIALOG_CONTEXT = Array.new def readmultiline(prompt = '', add_hist = false, &confirm_multiline_termination) - Reline.update_iogate - io_gate.with_raw_input do + @mutex.synchronize do unless confirm_multiline_termination raise ArgumentError.new('#readmultiline needs block to confirm multiline termination') end - inner_readline(prompt, add_hist, true, &confirm_multiline_termination) + + Reline.update_iogate + io_gate.with_raw_input do + inner_readline(prompt, add_hist, true, &confirm_multiline_termination) + end whole_buffer = line_editor.whole_buffer.dup whole_buffer.taint if RUBY_VERSION < '2.7' @@ -278,23 +274,32 @@ module Reline Reline::HISTORY << whole_buffer end - line_editor.reset_line if line_editor.whole_buffer.nil? - whole_buffer + if line_editor.eof? + line_editor.reset_line + # Return nil if the input is aborted by C-d. + nil + else + whole_buffer + end end end def readline(prompt = '', add_hist = false) - Reline.update_iogate - inner_readline(prompt, add_hist, false) + @mutex.synchronize do + Reline.update_iogate + io_gate.with_raw_input do + inner_readline(prompt, add_hist, false) + end - line = line_editor.line.dup - line.taint if RUBY_VERSION < '2.7' - if add_hist and line and line.chomp("\n").size > 0 - Reline::HISTORY << line.chomp("\n") - end + line = line_editor.line.dup + line.taint if RUBY_VERSION < '2.7' + if add_hist and line and line.chomp("\n").size > 0 + Reline::HISTORY << line.chomp("\n") + end - line_editor.reset_line if line_editor.line.nil? - line + line_editor.reset_line if line_editor.line.nil? + line + end end private def inner_readline(prompt, add_hist, multiline, &confirm_multiline_termination) @@ -307,6 +312,10 @@ module Reline $stderr.sync = true $stderr.puts "Reline is used by #{Process.pid}" end + unless config.test_mode or config.loaded? + config.read + io_gate.set_default_key_bindings(config) + end otio = io_gate.prep may_req_ambiguous_char_width @@ -326,58 +335,47 @@ module Reline line_editor.prompt_proc = prompt_proc line_editor.auto_indent_proc = auto_indent_proc line_editor.dig_perfect_match_proc = dig_perfect_match_proc - line_editor.pre_input_hook = pre_input_hook - @dialog_proc_list.each_pair do |name_sym, d| - line_editor.add_dialog_proc(name_sym, d.dialog_proc, d.context) - end - - unless config.test_mode - config.read - config.reset_default_key_bindings - io_gate.set_default_key_bindings(config) + pre_input_hook&.call + unless Reline::IOGate == Reline::GeneralIO + @dialog_proc_list.each_pair do |name_sym, d| + line_editor.add_dialog_proc(name_sym, d.dialog_proc, d.context) + end end + line_editor.print_nomultiline_prompt(prompt) + line_editor.update_dialogs line_editor.rerender begin line_editor.set_signal_handlers - prev_pasting_state = false loop do - prev_pasting_state = io_gate.in_pasting? read_io(config.keyseq_timeout) { |inputs| line_editor.set_pasting_state(io_gate.in_pasting?) - inputs.each { |c| - line_editor.input_key(c) - line_editor.rerender - } - if @bracketed_paste_finished - line_editor.rerender_all - @bracketed_paste_finished = false + inputs.each do |key| + if key.char == :bracketed_paste_start + text = io_gate.read_bracketed_paste + line_editor.insert_pasted_text(text) + line_editor.scroll_into_view + else + line_editor.update(key) + end end } - if prev_pasting_state == true and not io_gate.in_pasting? and not line_editor.finished? - line_editor.set_pasting_state(false) - prev_pasting_state = false - line_editor.rerender_all + if line_editor.finished? + line_editor.render_finished + break + else + line_editor.set_pasting_state(io_gate.in_pasting?) + line_editor.rerender end - break if line_editor.finished? end io_gate.move_cursor_column(0) rescue Errno::EIO # Maybe the I/O has been closed. - rescue StandardError => e + ensure line_editor.finalize io_gate.deprep(otio) - raise e - rescue Exception - # Including Interrupt - line_editor.finalize - io_gate.deprep(otio) - raise end - - line_editor.finalize - io_gate.deprep(otio) end # GNU Readline waits for "keyseq-timeout" milliseconds to see if the ESC @@ -395,7 +393,6 @@ module Reline c = io_gate.getc(Float::INFINITY) if c == -1 result = :unmatched - @bracketed_paste_finished = true else buffer << c result = key_stroke.match_status(buffer) diff --git a/lib/reline/ansi.rb b/lib/reline/ansi.rb index c2e5075ea8..45a475a787 100644 --- a/lib/reline/ansi.rb +++ b/lib/reline/ansi.rb @@ -3,6 +3,8 @@ require 'io/wait' require_relative 'terminfo' class Reline::ANSI + RESET_COLOR = "\e[0m" + CAPNAME_KEY_BINDINGS = { 'khome' => :ed_move_to_beg, 'kend' => :ed_move_to_end, @@ -43,6 +45,7 @@ class Reline::ANSI end def self.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) @@ -64,6 +67,12 @@ class Reline::ANSI end end + def self.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 self.set_default_key_bindings_ansi_cursor(config) ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)| bindings = [["\e[#{char}", default_func]] # CSI + char @@ -149,7 +158,11 @@ class Reline::ANSI end def self.with_raw_input - @@input.raw { yield } + if @@input.tty? + @@input.raw(intr: true) { yield } + else + yield + end end @@buf = [] @@ -157,11 +170,13 @@ class Reline::ANSI unless @@buf.empty? return @@buf.shift end - until c = @@input.raw(intr: true) { @@input.wait_readable(0.1) && @@input.getbyte } - timeout_second -= 0.1 + until @@input.wait_readable(0.01) + timeout_second -= 0.01 return nil if timeout_second <= 0 - Reline.core.line_editor.resize + + 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. @@ -170,46 +185,26 @@ class Reline::ANSI 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(timeout_second) + START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT) + END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT) + def self.read_bracketed_paste buffer = String.new(encoding: Encoding::ASCII_8BIT) - buffer << inner_getc(timeout_second) - while START_BRACKETED_PASTE.start_with?(buffer) or END_BRACKETED_PASTE.start_with?(buffer) do - if START_BRACKETED_PASTE == buffer - @@in_bracketed_paste_mode = true - return inner_getc(timeout_second) - elsif END_BRACKETED_PASTE == buffer - @@in_bracketed_paste_mode = false - ungetc(-1) - return inner_getc(timeout_second) - end - succ_c = inner_getc(Reline.core.config.keyseq_timeout) - - if succ_c - buffer << succ_c - else - break - end + until buffer.end_with?(END_BRACKETED_PASTE) + c = inner_getc(Float::INFINITY) + break unless c + buffer << c end - buffer.bytes.reverse_each do |ch| - ungetc ch - end - inner_getc(timeout_second) + 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 self.getc(timeout_second) - if Reline.core.config.enable_bracketed_paste - getc_with_bracketed_paste(timeout_second) - else - inner_getc(timeout_second) - end + inner_getc(timeout_second) end def self.in_pasting? - @@in_bracketed_paste_mode or (not empty_buffer?) + not empty_buffer? end def self.empty_buffer? @@ -276,7 +271,7 @@ class Reline::ANSI buf = @@output.pread(@@output.pos, 0) row = buf.count("\n") column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0 - rescue Errno::ESPIPE + rescue Errno::ESPIPE, IOError # Just returns column 1 for ambiguous width because this I/O is not # tty and can't seek. row = 0 @@ -307,7 +302,7 @@ class Reline::ANSI end def self.hide_cursor - if Reline::Terminfo.enabled? + if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? begin @@output.write Reline::Terminfo.tigetstr('civis') rescue Reline::Terminfo::TerminfoError @@ -319,7 +314,7 @@ class Reline::ANSI end def self.show_cursor - if Reline::Terminfo.enabled? + if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? begin @@output.write Reline::Terminfo.tigetstr('cnorm') rescue Reline::Terminfo::TerminfoError @@ -353,11 +348,15 @@ class Reline::ANSI end def self.prep + # Enable bracketed paste + @@output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste retrieve_keybuffer nil end def self.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/config.rb b/lib/reline/config.rb index 4c07a73701..d44c2675ab 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,11 +175,11 @@ class Reline::Config line = line.chomp.lstrip if line.start_with?('$') - handle_directive(line[1..-1], file, no) + handle_directive(line[1..-1], file, no, if_stack) next end - next if @skip_section + next if if_stack.any? { |_no, skip| skip } case line when /^set +([^ ]+) +([^ ]+)/i @@ -214,43 +188,47 @@ class Reline::Config 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(File.expand_path(args)) end diff --git a/lib/reline/face.rb b/lib/reline/face.rb index e18ec957e8..d07196e2e7 100644 --- a/lib/reline/face.rb +++ b/lib/reline/face.rb @@ -186,9 +186,9 @@ class Reline::Face conf.define :scrollbar, style: :reset end config(:completion_dialog) do |conf| - conf.define :default, foreground: :white, background: :cyan - conf.define :enhanced, foreground: :white, background: :magenta - conf.define :scrollbar, foreground: :white, background: :cyan + conf.define :default, foreground: :bright_white, background: :gray + conf.define :enhanced, foreground: :black, background: :white + conf.define :scrollbar, foreground: :white, background: :gray end end diff --git a/lib/reline/general_io.rb b/lib/reline/general_io.rb index eaae63f925..d52151ad3c 100644 --- a/lib/reline/general_io.rb +++ b/lib/reline/general_io.rb @@ -1,6 +1,8 @@ require 'io/wait' class Reline::GeneralIO + RESET_COLOR = '' # Do not send color reset sequence + def self.reset(encoding: nil) @@pasting = false if encoding @@ -44,6 +46,7 @@ class Reline::GeneralIO 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) @@ -57,7 +60,7 @@ class Reline::GeneralIO end def self.get_screen_size - [1, 1] + [24, 80] end def self.cursor_pos @@ -100,14 +103,6 @@ class Reline::GeneralIO @@pasting end - def self.start_pasting - @@pasting = true - end - - def self.finish_pasting - @@pasting = false - end - def self.prep end diff --git a/lib/reline/history.rb b/lib/reline/history.rb index 7a1ed6b90b..3f3b65fea6 100644 --- a/lib/reline/history.rb +++ b/lib/reline/history.rb @@ -62,7 +62,7 @@ class Reline::History < Array private def check_index(index) index += size if index < 0 if index < -2147483648 or 2147483647 < index - raise RangeError.new("integer #{index} too big to convert to `int'") + raise RangeError.new("integer #{index} too big to convert to 'int'") end # If history_size is negative, history size is unlimited. if @config.history_size.positive? diff --git a/lib/reline/key_actor/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..edd88289a3 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, - # 160 M-SPACE :ed_unassigned, + # 160 M-SPACE + :em_set_mark, # 161 M-! :ed_unassigned, # 162 M-" @@ -415,7 +415,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 206 M-N :vi_search_next, # 207 M-O - :ed_sequence_lead_in, + :ed_unassigned, # 208 M-P :vi_search_prev, # 209 M-Q @@ -431,15 +431,15 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 214 M-V :ed_unassigned, # 215 M-W - :em_copy_region, + :ed_unassigned, # 216 M-X - :ed_command, - # 217 M-Y :ed_unassigned, + # 217 M-Y + :em_yank_pop, # 218 M-Z :ed_unassigned, # 219 M-[ - :ed_sequence_lead_in, + :ed_unassigned, # 220 M-\ :ed_unassigned, # 221 M-] @@ -495,9 +495,9 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 246 M-v :ed_unassigned, # 247 M-w - :em_copy_region, + :ed_unassigned, # 248 M-x - :ed_command, + :ed_unassigned, # 249 M-y :ed_unassigned, # 250 M-z diff --git a/lib/reline/key_actor/vi_command.rb b/lib/reline/key_actor/vi_command.rb index 98146d2f77..06bb0ba8e4 100644 --- a/lib/reline/key_actor/vi_command.rb +++ b/lib/reline/key_actor/vi_command.rb @@ -17,7 +17,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 7 ^G :ed_unassigned, # 8 ^H - :ed_unassigned, + :ed_prev_char, # 9 ^I :ed_unassigned, # 10 ^J @@ -41,7 +41,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 19 ^S :ed_ignore, # 20 ^T - :ed_unassigned, + :ed_transpose_chars, # 21 ^U :vi_kill_line_prev, # 22 ^V @@ -51,7 +51,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 24 ^X :ed_unassigned, # 25 ^Y - :ed_unassigned, + :em_yank, # 26 ^Z :ed_unassigned, # 27 ^[ @@ -75,7 +75,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 36 $ :ed_move_to_end, # 37 % - :vi_match, + :ed_unassigned, # 38 & :ed_unassigned, # 39 ' @@ -89,11 +89,11 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 43 + :ed_next_history, # 44 , - :vi_repeat_prev_char, + :ed_unassigned, # 45 - :ed_prev_history, # 46 . - :vi_redo, + :ed_unassigned, # 47 / :vi_search_prev, # 48 0 @@ -117,9 +117,9 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 57 9 :ed_argument_digit, # 58 : - :ed_command, + :ed_unassigned, # 59 ; - :vi_repeat_next_char, + :ed_unassigned, # 60 < :ed_unassigned, # 61 = @@ -157,21 +157,21 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 77 M :ed_unassigned, # 78 N - :vi_repeat_search_prev, + :ed_unassigned, # 79 O - :ed_sequence_lead_in, + :ed_unassigned, # 80 P :vi_paste_prev, # 81 Q :ed_unassigned, # 82 R - :vi_replace_mode, + :ed_unassigned, # 83 S - :vi_substitute_line, + :ed_unassigned, # 84 T :vi_to_prev_char, # 85 U - :vi_undo_line, + :ed_unassigned, # 86 V :ed_unassigned, # 87 W @@ -179,11 +179,11 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 88 X :ed_delete_prev_char, # 89 Y - :vi_yank_end, + :ed_unassigned, # 90 Z :ed_unassigned, # 91 [ - :ed_sequence_lead_in, + :ed_unassigned, # 92 \ :ed_unassigned, # 93 ] @@ -191,7 +191,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 94 ^ :vi_first_print, # 95 _ - :vi_history_word, + :ed_unassigned, # 96 ` :ed_unassigned, # 97 a @@ -221,7 +221,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 109 m :ed_unassigned, # 110 n - :vi_repeat_search_next, + :ed_unassigned, # 111 o :ed_unassigned, # 112 p @@ -231,11 +231,11 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 114 r :vi_replace_char, # 115 s - :vi_substitute_char, + :ed_unassigned, # 116 t :vi_to_next_char, # 117 u - :vi_undo, + :ed_unassigned, # 118 v :vi_histedit, # 119 w @@ -253,9 +253,9 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 125 } :ed_unassigned, # 126 ~ - :vi_change_case, - # 127 ^? :ed_unassigned, + # 127 ^? + :em_delete_prev_char, # 128 M-^@ :ed_unassigned, # 129 M-^A @@ -415,7 +415,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 206 M-N :ed_unassigned, # 207 M-O - :ed_sequence_lead_in, + :ed_unassigned, # 208 M-P :ed_unassigned, # 209 M-Q @@ -439,7 +439,7 @@ class Reline::KeyActor::ViCommand < Reline::KeyActor::Base # 218 M-Z :ed_unassigned, # 219 M-[ - :ed_sequence_lead_in, + :ed_unassigned, # 220 M-\ :ed_unassigned, # 221 M-] diff --git a/lib/reline/key_actor/vi_insert.rb b/lib/reline/key_actor/vi_insert.rb index b8e89f81d8..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/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 d71b903701..23ece60220 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,28 +32,49 @@ 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(:preposing, :postposing, :list, :pointer) - MenuInfo = Struct.new(: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 @@ -65,73 +83,42 @@ class Reline::LineEditor 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 - 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 - 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? @@ -141,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 = "''" @@ -182,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 @@ -259,56 +225,43 @@ 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) + @past_lines = [] + @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 @@ -319,68 +272,44 @@ class Reline::LineEditor @is_multiline = false end - private def calculate_height_by_lines(lines, prompt) - result = 0 - prompt_list = prompt.is_a?(Array) ? prompt : nil - lines.each_with_index { |line, i| - prompt = prompt_list[i] if prompt_list and prompt_list[i] - result += calculate_height_by_width(calculate_width(prompt, true) + calculate_width(line)) - } - result - end - private def insert_new_line(cursor_line, next_line) - @line = cursor_line @buffer_of_lines.insert(@line_index + 1, String.new(next_line, encoding: @encoding)) - @previous_line_index = @line_index + @buffer_of_lines[@line_index] = cursor_line @line_index += 1 - @just_cursor_moving = false - end - - private def calculate_height_by_width(width) - width.div(@screen_size.last) + 1 - end - - private def split_by_width(str, max_width) - Reline::Unicode.split_by_width(str, max_width, @encoding) - end - - private def scroll_down(val) - if val <= @rest_height - Reline::IOGate.move_cursor_down(val) - @rest_height -= val - else - Reline::IOGate.move_cursor_down(@rest_height) - Reline::IOGate.scroll_down(val - @rest_height) - @rest_height = 0 + @byte_pointer = 0 + if @auto_indent_proc && !@in_pasting + if next_line.empty? + ( + # For compatibility, use this calculation instead of just `process_auto_indent @line_index - 1, cursor_dependent: false` + indent1 = @auto_indent_proc.(@buffer_of_lines.take(@line_index - 1).push(''), @line_index - 1, 0, true) + indent2 = @auto_indent_proc.(@buffer_of_lines.take(@line_index), @line_index - 1, @buffer_of_lines[@line_index - 1].bytesize, false) + indent = indent2 || indent1 + @buffer_of_lines[@line_index - 1] = ' ' * indent + @buffer_of_lines[@line_index - 1].gsub(/\A\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 @@ -406,113 +335,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}#{' ' * 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}#{content}#{Reline::IOGate::RESET_COLOR}" 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 @@ -563,21 +621,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 @@ -646,27 +703,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 @@ -674,106 +710,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 @@ -813,23 +752,22 @@ 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] @@ -838,7 +776,7 @@ class Reline::LineEditor dialog.contents = contents.map.with_index do |item, i| line_sgr = i == pointer ? enhanced_sgr : default_sgr str_width = dialog.width - (scrollbar_pos.nil? ? 0 : @block_elem_width) - str = padding_space_with_escape_sequences(Reline::Unicode.take_range(item, 0, str_width), str_width) + str, = Reline::Unicode.take_mbchar_range(item, 0, str_width, padding: true) colored_content = "#{line_sgr}#{str}" if scrollbar_pos if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height) @@ -856,379 +794,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) @@ -1256,7 +835,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]) @@ -1277,9 +856,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) @@ -1306,100 +885,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 @@ -1416,7 +975,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 @@ -1435,11 +994,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) @@ -1451,7 +1032,6 @@ class Reline::LineEditor end @kill_ring.process if @vi_arg - @rerender_al = true @vi_arg = nil end elsif @vi_arg @@ -1462,8 +1042,6 @@ class Reline::LineEditor run_for_operators(key, method_symbol) do |with_operator| wrap_method_call(method_symbol, method_obj, key, with_operator) end - elsif @waiting_proc - @waiting_proc.(key) elsif method_obj wrap_method_call(method_symbol, method_obj, key) else @@ -1471,13 +1049,9 @@ class Reline::LineEditor end @kill_ring.process if @vi_arg - @rerender_all = true @vi_arg = nil end end - elsif @waiting_proc - @waiting_proc.(key) - @kill_ring.process elsif method_obj if method_symbol == :ed_argument_digit wrap_method_call(method_symbol, method_obj, key) @@ -1493,11 +1067,6 @@ class Reline::LineEditor end private def normal_char(key) - method_symbol = method_obj = nil - if key.combined_char.is_a?(Symbol) - process_key(key.combined_char, key.combined_char) - return - end @multibyte_buffer << key.combined_char if @multibyte_buffer.size > 1 if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding? @@ -1523,87 +1092,95 @@ class Reline::LineEditor 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 - else - @just_cursor_moving = false + + push_past_lines unless @undoing + @undoing = false + + if @in_pasting + clear_dialogs + return end - if @is_multiline and @auto_indent_proc and not simplified_rendering? and @line - process_auto_indent + + 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 + @old_byte_pointer = @byte_pointer.dup + @old_line_index = @line_index.dup + end + + def push_past_lines + if @old_buffer_of_lines != @buffer_of_lines + @past_lines.push([@old_buffer_of_lines, @old_byte_pointer, @old_line_index]) + end + trim_past_lines + end + + MAX_PAST_LINES = 100 + def trim_past_lines + if @past_lines.size > MAX_PAST_LINES + @past_lines.shift + 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 @@ -1637,43 +1214,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[@line_index - 1].bytesize + 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 + 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 - @check_new_auto_indent = false + 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) @@ -1687,7 +1273,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 @@ -1695,7 +1281,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 @@ -1717,15 +1303,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) @@ -1736,126 +1322,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_past_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? @@ -1864,7 +1417,6 @@ class Reline::LineEditor def finish @finished = true - @rerender_all = true @config.reset end @@ -1895,33 +1447,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 @@ -1939,9 +1514,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) @@ -1949,7 +1521,6 @@ class Reline::LineEditor return end str = key - bytesize = key.bytesize else begin key.chr.encode(Encoding::UTF_8) @@ -1957,7 +1528,6 @@ class Reline::LineEditor return end str = key.chr - bytesize = 1 end if @in_pasting @continuous_insertion_buffer << str @@ -1965,28 +1535,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 @@ -2008,18 +1558,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 @@ -2028,19 +1571,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 @@ -2048,157 +1584,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 @@ -2210,67 +1698,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 } @@ -2286,199 +1748,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 @@ -2503,40 +1861,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 @@ -2546,23 +1893,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. @@ -2570,11 +1917,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 @@ -2584,50 +1929,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 @@ -2635,164 +1965,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 @@ -2820,10 +2122,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 @@ -2831,38 +2132,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 @@ -2870,50 +2165,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 @@ -2928,78 +2212,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 @@ -3010,18 +2297,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 @@ -3032,54 +2316,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 @@ -3087,11 +2342,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 @@ -3115,43 +2368,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 } @@ -3174,7 +2417,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 @@ -3194,21 +2437,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 @@ -3231,7 +2469,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 @@ -3251,26 +2489,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 @@ -3284,14 +2515,27 @@ 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 em_meta_next(key) + 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) + return if @past_lines.empty? + + @undoing = true + + target_lines, target_cursor_x, target_cursor_y = @past_lines.last + set_current_lines(target_lines, target_cursor_x, target_cursor_y) + + @past_lines.pop 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 26ef207ba6..d7460d6d4a 100644 --- a/lib/reline/unicode.rb +++ b/lib/reline/unicode.rb @@ -43,11 +43,13 @@ class Reline::Unicode 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 @@ -128,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) @@ -145,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 @@ -173,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/version.rb b/lib/reline/version.rb index 194d16e69a..46613a5952 100644 --- a/lib/reline/version.rb +++ b/lib/reline/version.rb @@ -1,3 +1,3 @@ module Reline - VERSION = '0.4.1' + VERSION = '0.5.7' end diff --git a/lib/reline/windows.rb b/lib/reline/windows.rb index 6f635f630f..ee3f73e383 100644 --- a/lib/reline/windows.rb +++ b/lib/reline/windows.rb @@ -1,6 +1,8 @@ require 'fiddle/import' class Reline::Windows + RESET_COLOR = "\e[0m" + def self.encoding Encoding::UTF_8 end @@ -85,7 +87,7 @@ class Reline::Windows def call(*args) import = @proto.split("") args.each_with_index do |x, i| - args[i], = [x == 0 ? nil : x].pack("p").unpack(POINTER_TYPE) if import[i] == "S" + args[i], = [x == 0 ? nil : +x].pack("p").unpack(POINTER_TYPE) if import[i] == "S" args[i], = [x].pack("I").unpack("i") if import[i] == "I" end ret, = @func.call(*args) @@ -257,7 +259,7 @@ class Reline::Windows def self.check_input_event num_of_events = 0.chr * 8 while @@output_buf.empty? - Reline.core.line_editor.resize + Reline.core.line_editor.handle_signal if @@WaitForSingleObject.(@@hConsoleInputHandle, 100) != 0 # max 0.1 sec # prevent for background consolemode change @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) diff --git a/test/irb/command/test_custom_command.rb b/test/irb/command/test_custom_command.rb new file mode 100644 index 0000000000..3a3ad11d5a --- /dev/null +++ b/test/irb/command/test_custom_command.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true +require "irb" + +require_relative "../helper" + +module TestIRB + class CustomCommandIntegrationTest < TestIRB::IntegrationTestCase + def test_command_registration_can_happen_after_irb_require + write_ruby <<~RUBY + require "irb" + require "irb/command" + + class PrintCommand < IRB::Command::Base + category 'CommandTest' + description 'print_command' + def execute(*) + puts "Hello from PrintCommand" + end + end + + IRB::Command.register(:print!, PrintCommand) + + binding.irb + RUBY + + output = run_ruby_file do + type "print!" + type "exit" + end + + assert_include(output, "Hello from PrintCommand") + end + + def test_command_registration_accepts_string_too + write_ruby <<~RUBY + require "irb/command" + + class PrintCommand < IRB::Command::Base + category 'CommandTest' + description 'print_command' + def execute(*) + puts "Hello from PrintCommand" + end + end + + IRB::Command.register("print!", PrintCommand) + + binding.irb + RUBY + + output = run_ruby_file do + type "print!" + type "exit" + end + + assert_include(output, "Hello from PrintCommand") + end + + def test_arguments_propagation + write_ruby <<~RUBY + require "irb/command" + + class PrintArgCommand < IRB::Command::Base + category 'CommandTest' + description 'print_command_arg' + def execute(arg) + $nth_execution ||= 0 + puts "\#{$nth_execution} arg=\#{arg.inspect}" + $nth_execution += 1 + end + end + + IRB::Command.register(:print_arg, PrintArgCommand) + + binding.irb + RUBY + + output = run_ruby_file do + type "print_arg" + type "print_arg \n" + type "print_arg a r g" + type "print_arg a r g \n" + type "exit" + end + + assert_include(output, "0 arg=\"\"") + assert_include(output, "1 arg=\"\"") + assert_include(output, "2 arg=\"a r g\"") + assert_include(output, "3 arg=\"a r g\"") + end + + def test_def_extend_command_still_works + write_ruby <<~RUBY + require "irb" + + class FooBarCommand < IRB::Command::Base + category 'FooBarCategory' + description 'foobar_description' + def execute(*) + $nth_execution ||= 1 + puts "\#{$nth_execution} FooBar executed" + $nth_execution += 1 + end + end + + IRB::ExtendCommandBundle.def_extend_command(:foobar, FooBarCommand, nil, [:fbalias, IRB::Command::OVERRIDE_ALL]) + + binding.irb + RUBY + + output = run_ruby_file do + type "foobar" + type "fbalias" + type "help foobar" + type "exit" + end + + assert_include(output, "1 FooBar executed") + assert_include(output, "2 FooBar executed") + assert_include(output, "foobar_description") + end + + def test_no_meta_command_also_works + write_ruby <<~RUBY + require "irb/command" + + class NoMetaCommand < IRB::Command::Base + def execute(*) + puts "This command does not override meta attributes" + end + end + + IRB::Command.register(:no_meta, NoMetaCommand) + + binding.irb + RUBY + + output = run_ruby_file do + type "no_meta" + type "help no_meta" + type "exit" + end + + assert_include(output, "This command does not override meta attributes") + assert_include(output, "No description provided.") + assert_not_include(output, "Maybe IRB bug") + end + end +end diff --git a/test/irb/command/test_force_exit.rb b/test/irb/command/test_force_exit.rb new file mode 100644 index 0000000000..191a786872 --- /dev/null +++ b/test/irb/command/test_force_exit.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: false +require 'irb' + +require_relative "../helper" + +module TestIRB + class ForceExitTest < IntegrationTestCase + def test_forced_exit_finishes_process_immediately + write_ruby <<~'ruby' + puts "First line" + puts "Second line" + binding.irb + puts "Third line" + binding.irb + puts "Fourth line" + ruby + + output = run_ruby_file do + type "123" + type "456" + type "exit!" + end + + assert_match(/First line\r\n/, output) + assert_match(/Second line\r\n/, output) + assert_match(/irb\(main\):001> 123/, output) + assert_match(/irb\(main\):002> 456/, output) + refute_match(/Third line\r\n/, output) + refute_match(/Fourth line\r\n/, output) + end + + def test_forced_exit_in_nested_sessions + write_ruby <<~'ruby' + def foo + binding.irb + end + + binding.irb + binding.irb + ruby + + output = run_ruby_file do + type "123" + type "foo" + type "exit!" + end + + assert_match(/irb\(main\):001> 123/, output) + end + end +end diff --git a/test/irb/command/test_help.rb b/test/irb/command/test_help.rb new file mode 100644 index 0000000000..b34832b022 --- /dev/null +++ b/test/irb/command/test_help.rb @@ -0,0 +1,75 @@ +require "tempfile" +require_relative "../helper" + +module TestIRB + class HelpTest < IntegrationTestCase + def setup + super + + write_rc <<~'RUBY' + IRB.conf[:USE_PAGER] = false + RUBY + + write_ruby <<~'RUBY' + binding.irb + RUBY + end + + def test_help + out = run_ruby_file do + type "help" + type "exit" + end + + assert_match(/List all available commands/, out) + assert_match(/Start the debugger of debug\.gem/, out) + end + + def test_command_help + out = run_ruby_file do + type "help ls" + type "exit" + end + + assert_match(/Usage: ls \[obj\]/, out) + end + + def test_command_help_not_found + out = run_ruby_file do + type "help foo" + type "exit" + end + + assert_match(/Can't find command `foo`\. Please check the command name and try again\./, out) + end + + def test_show_cmds + out = run_ruby_file do + type "help" + type "exit" + end + + assert_match(/List all available commands/, out) + assert_match(/Start the debugger of debug\.gem/, out) + end + + def test_help_lists_user_aliases + out = run_ruby_file do + type "help" + type "exit" + end + + assert_match(/\$\s+Alias for `show_source`/, out) + assert_match(/@\s+Alias for `whereami`/, out) + end + + def test_help_lists_helper_methods + out = run_ruby_file do + type "help" + type "exit" + end + + assert_match(/Helper methods\s+conf\s+Returns the current IRB context/, out) + end + end +end diff --git a/test/irb/command/test_multi_irb_commands.rb b/test/irb/command/test_multi_irb_commands.rb new file mode 100644 index 0000000000..e313c0c5d2 --- /dev/null +++ b/test/irb/command/test_multi_irb_commands.rb @@ -0,0 +1,50 @@ +require "tempfile" +require_relative "../helper" + +module TestIRB + class MultiIRBTest < IntegrationTestCase + def setup + super + + write_ruby <<~'RUBY' + binding.irb + RUBY + end + + def test_jobs_command_with_print_deprecated_warning + out = run_ruby_file do + type "jobs" + type "exit" + end + + assert_match(/Multi-irb commands are deprecated and will be removed in IRB 2\.0\.0\. Please use workspace commands instead\./, out) + assert_match(%r|If you have any use case for multi-irb, please leave a comment at https://github.com/ruby/irb/issues/653|, out) + assert_match(/#0->irb on main \(#<Thread:0x.+ run>: running\)/, out) + end + + def test_irb_jobs_and_kill_commands + out = run_ruby_file do + type "irb" + type "jobs" + type "kill 1" + type "exit" + end + + assert_match(/#0->irb on main \(#<Thread:0x.+ sleep_forever>: stop\)/, out) + assert_match(/#1->irb#1 on main \(#<Thread:0x.+ run>: running\)/, out) + end + + def test_irb_fg_jobs_and_kill_commands + out = run_ruby_file do + type "irb" + type "fg 0" + type "jobs" + type "kill 1" + type "exit" + end + + assert_match(/#0->irb on main \(#<Thread:0x.+ run>: running\)/, out) + assert_match(/#1->irb#1 on main \(#<Thread:0x.+ sleep_forever>: stop\)/, out) + end + end +end diff --git a/test/irb/cmd/test_show_source.rb b/test/irb/command/test_show_source.rb index cedec8aa6f..d014c78fc4 100644 --- a/test/irb/cmd/test_show_source.rb +++ b/test/irb/command/test_show_source.rb @@ -52,6 +52,19 @@ module TestIRB assert_match(%r[Couldn't locate a definition for foo], out) end + def test_show_source_with_missing_constant + write_ruby <<~'RUBY' + binding.irb + RUBY + + out = run_ruby_file do + type "show_source Foo" + type "exit" + end + + assert_match(%r[Couldn't locate a definition for Foo], out) + end + def test_show_source_string write_ruby <<~'RUBY' binding.irb @@ -272,5 +285,113 @@ module TestIRB assert_match(%r[#{@ruby_file.to_path}:2\s+def foo\r\n end], out) end + + def test_show_source_with_double_colons + write_ruby <<~RUBY + class Foo + end + + class Foo + class Bar + end + end + + binding.irb + RUBY + + out = run_ruby_file do + type "show_source ::Foo" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:1\s+class Foo\r\nend], out) + + out = run_ruby_file do + type "show_source ::Foo::Bar" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:5\s+class Bar\r\n end], out) + end + + def test_show_source_keep_script_lines + pend unless defined?(RubyVM.keep_script_lines) + + write_ruby <<~RUBY + binding.irb + RUBY + + out = run_ruby_file do + type "def foo; end" + type "show_source foo" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}\(irb\):1\s+def foo; end], out) + end + + def test_show_source_unavailable_source + write_ruby <<~RUBY + binding.irb + RUBY + + out = run_ruby_file do + type "RubyVM.keep_script_lines = false if defined?(RubyVM.keep_script_lines)" + type "def foo; end" + type "show_source foo" + type "exit" + end + assert_match(%r[#{@ruby_file.to_path}\(irb\):2\s+Source not available], out) + end + + def test_show_source_shows_binary_source + write_ruby <<~RUBY + # io-console is an indirect dependency of irb + require "io/console" + + binding.irb + RUBY + + out = run_ruby_file do + # IO::ConsoleMode is defined in io-console gem's C extension + type "show_source IO::ConsoleMode" + type "exit" + end + + # A safeguard to make sure the test subject is actually defined + refute_match(/NameError/, out) + assert_match(%r[Defined in binary file:.+io/console], out) + end + + def test_show_source_with_constant_lookup + write_ruby <<~RUBY + X = 1 + module M + Y = 1 + Z = 2 + end + class A + Z = 1 + Array = 1 + class B + include M + Object.new.instance_eval { binding.irb } + end + end + RUBY + + out = run_ruby_file do + type "show_source X" + type "show_source Y" + type "show_source Z" + type "show_source Array" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:1\s+X = 1], out) + assert_match(%r[#{@ruby_file.to_path}:3\s+Y = 1], out) + assert_match(%r[#{@ruby_file.to_path}:7\s+Z = 1], out) + assert_match(%r[#{@ruby_file.to_path}:8\s+Array = 1], out) + end end end diff --git a/test/irb/helper.rb b/test/irb/helper.rb index 38bdbb4c31..acaf6277f3 100644 --- a/test/irb/helper.rb +++ b/test/irb/helper.rb @@ -1,5 +1,6 @@ require "test/unit" require "pathname" +require "rubygems" begin require_relative "../lib/helper" @@ -17,6 +18,7 @@ module IRB end module TestIRB + RUBY_3_4 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0.dev") class TestCase < Test::Unit::TestCase class TestInputMethod < ::IRB::InputMethod attr_reader :list, :line_no @@ -119,7 +121,9 @@ module TestIRB @envs["XDG_CONFIG_HOME"] ||= tmp_dir @envs["IRBRC"] = nil unless @envs.key?("IRBRC") - PTY.spawn(@envs.merge("TERM" => "dumb"), *cmd) do |read, write, pid| + envs_for_spawn = @envs.merge('TERM' => 'dumb', 'TEST_IRB_FORCE_INTERACTIVE' => 'true') + + PTY.spawn(envs_for_spawn, *cmd) do |read, write, pid| Timeout.timeout(TIMEOUT_SEC) do while line = safe_gets(read) lines << line @@ -194,7 +198,7 @@ module TestIRB end def write_ruby(program) - @ruby_file = Tempfile.create(%w{irb- .rb}) + @ruby_file = Tempfile.create(%w{irbtest- .rb}) @tmpfiles << @ruby_file @ruby_file.write(program) @ruby_file.close diff --git a/test/irb/test_color.rb b/test/irb/test_color.rb index c9d2512dc5..9d78f5233e 100644 --- a/test/irb/test_color.rb +++ b/test/irb/test_color.rb @@ -1,6 +1,5 @@ # frozen_string_literal: false require 'irb/color' -require 'rubygems' require 'stringio' require_relative "helper" @@ -100,7 +99,7 @@ module TestIRB "foo %i[bar]" => "foo #{YELLOW}%i[#{CLEAR}#{YELLOW}bar#{CLEAR}#{YELLOW}]#{CLEAR}", "foo :@bar, baz, :@@qux, :$quux" => "foo #{YELLOW}:#{CLEAR}#{YELLOW}@bar#{CLEAR}, baz, #{YELLOW}:#{CLEAR}#{YELLOW}@@qux#{CLEAR}, #{YELLOW}:#{CLEAR}#{YELLOW}$quux#{CLEAR}", "`echo`" => "#{RED}#{BOLD}`#{CLEAR}#{RED}echo#{CLEAR}#{RED}#{BOLD}`#{CLEAR}", - "\t" => "\t", # not ^I + "\t" => Reline::Unicode.escape_for_print("\t") == ' ' ? ' ' : "\t", # not ^I "foo(*%W(bar))" => "foo(*#{RED}#{BOLD}%W(#{CLEAR}#{RED}bar#{CLEAR}#{RED}#{BOLD})#{CLEAR})", "$stdout" => "#{GREEN}#{BOLD}$stdout#{CLEAR}", "__END__" => "#{GREEN}__END__#{CLEAR}", diff --git a/test/irb/test_color_printer.rb b/test/irb/test_color_printer.rb index f162b88548..c2c624d868 100644 --- a/test/irb/test_color_printer.rb +++ b/test/irb/test_color_printer.rb @@ -1,6 +1,5 @@ # frozen_string_literal: false require 'irb/color_printer' -require 'rubygems' require 'stringio' require_relative "helper" diff --git a/test/irb/test_cmd.rb b/test/irb/test_command.rb index d99ac05c5d..8cb8928adb 100644 --- a/test/irb/test_cmd.rb +++ b/test/irb/test_command.rb @@ -1,7 +1,5 @@ # frozen_string_literal: false -require "rubygems" require "irb" -require "irb/extend-command" require_relative "helper" @@ -22,6 +20,7 @@ module TestIRB @xdg_config_home_backup = ENV.delete("XDG_CONFIG_HOME") save_encodings IRB.instance_variable_get(:@CONF).clear + IRB.instance_variable_set(:@existing_rc_name_generators, nil) @is_win = (RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/) end @@ -34,17 +33,17 @@ module TestIRB end def execute_lines(*lines, conf: {}, main: self, irb_path: nil) - IRB.init_config(nil) - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :SIMPLE - IRB.conf[:USE_PAGER] = false - IRB.conf.merge!(conf) - input = TestInputMethod.new(lines) - irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) - irb.context.return_format = "=> %s\n" - irb.context.irb_path = irb_path if irb_path - IRB.conf[:MAIN_CONTEXT] = irb.context capture_output do + IRB.init_config(nil) + IRB.conf[:VERBOSE] = false + IRB.conf[:PROMPT_MODE] = :SIMPLE + IRB.conf[:USE_PAGER] = false + IRB.conf.merge!(conf) + input = TestInputMethod.new(lines) + irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) + irb.context.return_format = "=> %s\n" + irb.context.irb_path = irb_path if irb_path + IRB.conf[:MAIN_CONTEXT] = irb.context irb.eval_input end end @@ -58,7 +57,7 @@ module TestIRB "irb_info", main: main ) - assert_empty err + assert_empty(err) assert_match(/RUBY_PLATFORM/, out) end end @@ -77,6 +76,7 @@ module TestIRB def test_irb_info_multiline FileUtils.touch("#{@tmpdir}/.inputrc") FileUtils.touch("#{@tmpdir}/.irbrc") + FileUtils.touch("#{@tmpdir}/_irbrc") out, err = execute_lines( "irb_info", @@ -88,7 +88,7 @@ module TestIRB IRB\sversion:\sirb\s.+\n InputMethod:\sAbstract\sInputMethod\n Completion: .+\n - \.irbrc\spath:\s.+\n + \.irbrc\spaths:.*\.irbrc.*_irbrc\n RUBY_PLATFORM:\s.+\n East\sAsian\sAmbiguous\sWidth:\s\d\n #{@is_win ? 'Code\spage:\s\d+\n' : ''} @@ -112,7 +112,7 @@ module TestIRB IRB\sversion:\sirb\s.+\n InputMethod:\sAbstract\sInputMethod\n Completion: .+\n - \.irbrc\spath:\s.+\n + \.irbrc\spaths:\s.+\n RUBY_PLATFORM:\s.+\n East\sAsian\sAmbiguous\sWidth:\s\d\n #{@is_win ? 'Code\spage:\s\d+\n' : ''} @@ -198,7 +198,7 @@ module TestIRB IRB\sversion:\sirb .+\n InputMethod:\sAbstract\sInputMethod\n Completion: .+\n - \.irbrc\spath: .+\n + \.irbrc\spaths: .+\n RUBY_PLATFORM: .+\n LANG\senv:\sja_JP\.UTF-8\n LC_ALL\senv:\sen_US\.UTF-8\n @@ -373,17 +373,19 @@ module TestIRB } } out, err = execute_lines( - "measure :foo", - "measure :on, :bar", - "3\n", + "measure :foo\n", + "1\n", + "measure :on, :bar\n", + "2\n", "measure :off, :foo\n", - "measure :off, :bar\n", "3\n", + "measure :off, :bar\n", + "4\n", conf: conf ) assert_empty err - assert_match(/\AFOO is added\.\n=> nil\nfoo\nBAR is added\.\n=> nil\nbar\nfoo\n=> 3\nbar\nfoo\n=> nil\nbar\n=> nil\n=> 3\n/, out) + assert_match(/\AFOO is added\.\n=> nil\nfoo\n=> 1\nBAR is added\.\n=> nil\nbar\nfoo\n=> 2\n=> nil\nbar\n=> 3\n=> nil\n=> 4\n/, out) end def test_measure_with_proc_warning @@ -402,7 +404,6 @@ module TestIRB out, err = execute_lines( "3\n", "measure do\n", - "end\n", "3\n", conf: conf, main: c @@ -484,62 +485,67 @@ module TestIRB class CwwsTest < WorkspaceCommandTestCase def test_cwws_returns_the_current_workspace_object out, err = execute_lines( - "cwws.class", + "cwws" ) assert_empty err - assert_include(out, self.class.name) + assert_include(out, "Current workspace: #{self}") end end class PushwsTest < WorkspaceCommandTestCase def test_pushws_switches_to_new_workspace_and_pushes_the_current_one_to_the_stack out, err = execute_lines( - "pushws #{self.class}::Foo.new\n", - "cwws.class", + "pushws #{self.class}::Foo.new", + "self.class", + "popws", + "self.class" ) assert_empty err - assert_include(out, "#{self.class}::Foo") + + assert_match(/=> #{self.class}::Foo\n/, out) + assert_match(/=> #{self.class}\n$/, out) end def test_pushws_extends_the_new_workspace_with_command_bundle out, err = execute_lines( - "pushws Object.new\n", + "pushws Object.new", "self.singleton_class.ancestors" ) assert_empty err assert_include(out, "IRB::ExtendCommandBundle") end - def test_pushws_prints_help_message_when_no_arg_is_given + def test_pushws_prints_workspace_stack_when_no_arg_is_given out, err = execute_lines( - "pushws\n", + "pushws", ) assert_empty err - assert_match(/No other workspace/, out) + assert_include(out, "[#<TestIRB::PushwsTe...>]") end - end - class WorkspacesTest < WorkspaceCommandTestCase - def test_workspaces_returns_the_array_of_non_main_workspaces + def test_pushws_without_argument_swaps_the_top_two_workspaces out, err = execute_lines( - "pushws #{self.class}::Foo.new\n", - "workspaces.map { |w| w.class.name }", + "pushws #{self.class}::Foo.new", + "self.class", + "pushws", + "self.class" ) - assert_empty err - # self.class::Foo would be the current workspace - # self.class would be the old workspace that's pushed to the stack - assert_include(out, "=> [\"#{self.class}\"]") + assert_match(/=> #{self.class}::Foo\n/, out) + assert_match(/=> #{self.class}\n$/, out) end + end - def test_workspaces_returns_empty_array_when_no_workspaces_were_added + class WorkspacesTest < WorkspaceCommandTestCase + def test_workspaces_returns_the_stack_of_workspaces out, err = execute_lines( - "workspaces.map(&:to_s)", + "pushws #{self.class}::Foo.new\n", + "workspaces", ) assert_empty err - assert_include(out, "=> []") + assert_match(/\[#<TestIRB::Workspac...>, #<TestIRB::Workspac...>\]\n/, out) end end @@ -548,7 +554,8 @@ module TestIRB out, err = execute_lines( "pushws Foo.new\n", "popws\n", - "cwws.class", + "cwws\n", + "self.class", ) assert_empty err assert_include(out, "=> #{self.class}") @@ -559,7 +566,7 @@ module TestIRB "popws\n", ) assert_empty err - assert_match(/workspace stack empty/, out) + assert_match(/\[#<TestIRB::PopwsTes...>\]\n/, out) end end @@ -567,19 +574,20 @@ module TestIRB def test_chws_replaces_the_current_workspace out, err = execute_lines( "chws #{self.class}::Foo.new\n", - "cwws.class", + "cwws\n", + "self.class\n" ) assert_empty err + assert_include(out, "Current workspace: #<#{self.class.name}::Foo") assert_include(out, "=> #{self.class}::Foo") end def test_chws_does_nothing_when_receiving_no_argument out, err = execute_lines( "chws\n", - "cwws.class", ) assert_empty err - assert_include(out, "=> #{self.class}") + assert_include(out, "Current workspace: #{self}") end end @@ -601,29 +609,6 @@ module TestIRB end end - - class ShowCmdsTest < CommandTestCase - def test_show_cmds - out, err = execute_lines( - "show_cmds\n" - ) - - assert_empty err - assert_match(/List all available commands and their description/, out) - assert_match(/Start the debugger of debug\.gem/, out) - end - - def test_show_cmds_list_user_aliases - out, err = execute_lines( - "show_cmds\n" - ) - - assert_empty err - assert_match(/\$\s+Alias for `show_source`/, out) - assert_match(/@\s+Alias for `whereami`/, out) - end - end - class LsTest < CommandTestCase def test_ls out, err = execute_lines( @@ -747,18 +732,18 @@ module TestIRB def test_ls_grep_empty out, err = execute_lines("ls\n") assert_empty err - assert_match(/whereami/, out) - assert_match(/show_source/, out) + assert_match(/assert/, out) + assert_match(/refute/, out) [ - "ls grep: /whereami/\n", - "ls -g whereami\n", - "ls -G whereami\n", + "ls grep: /assert/\n", + "ls -g assert\n", + "ls -G assert\n", ].each do |line| out, err = execute_lines(line) assert_empty err - assert_match(/whereami/, out) - assert_not_match(/show_source/, out) + assert_match(/assert/, out) + assert_not_match(/refute/, out) end end @@ -774,22 +759,6 @@ module TestIRB end class ShowDocTest < CommandTestCase - def test_help - out, err = execute_lines( - "help String#gsub\n", - "\n", - ) - - # the former is what we'd get without document content installed, like on CI - # the latter is what we may get locally - possible_rdoc_output = [/Nothing known about String#gsub/, /gsub\(pattern\)/] - assert_include err, "[Deprecation] The `help` command will be repurposed to display command help in the future.\n" - assert(possible_rdoc_output.any? { |output| output.match?(out) }, "Expect the `help` command to match one of the possible outputs. Got:\n#{out}") - ensure - # this is the only way to reset the redefined method without coupling the test with its implementation - EnvUtil.suppress_warning { load "irb/cmd/help.rb" } - end - def test_show_doc out, err = execute_lines( "show_doc String#gsub\n", @@ -803,7 +772,7 @@ module TestIRB assert(possible_rdoc_output.any? { |output| output.match?(out) }, "Expect the `show_doc` command to match one of the possible outputs. Got:\n#{out}") ensure # this is the only way to reset the redefined method without coupling the test with its implementation - EnvUtil.suppress_warning { load "irb/cmd/help.rb" } + EnvUtil.suppress_warning { load "irb/command/help.rb" } end def test_show_doc_without_rdoc @@ -819,7 +788,7 @@ module TestIRB assert_include(err, "Can't display document because `rdoc` is not installed.\n") ensure # this is the only way to reset the redefined method without coupling the test with its implementation - EnvUtil.suppress_warning { load "irb/cmd/help.rb" } + EnvUtil.suppress_warning { load "irb/command/help.rb" } end end @@ -848,6 +817,16 @@ module TestIRB assert_match("command: ': code'", out) end + def test_edit_without_arg_and_non_existing_irb_path + out, err = execute_lines( + "edit", + irb_path: '/path/to/file.rb(irb)' + ) + + assert_empty err + assert_match(/Can not find file: \/path\/to\/file\.rb\(irb\)/, out) + end + def test_edit_with_path out, err = execute_lines( "edit #{__FILE__}" @@ -974,4 +953,19 @@ module TestIRB end + class HelperMethodInsallTest < CommandTestCase + def test_helper_method_install + IRB::ExtendCommandBundle.module_eval do + def foobar + "test_helper_method_foobar" + end + end + + out, err = execute_lines("foobar.upcase") + assert_empty err + assert_include(out, '=> "TEST_HELPER_METHOD_FOOBAR"') + ensure + IRB::ExtendCommandBundle.remove_method :foobar + end + end end diff --git a/test/irb/test_completion.rb b/test/irb/test_completion.rb index 0625c96976..5fe7952b3d 100644 --- a/test/irb/test_completion.rb +++ b/test/irb/test_completion.rb @@ -14,6 +14,13 @@ module TestIRB IRB::RegexpCompletor.new.doc_namespace('', target, '', bind: bind) end + class CommandCompletionTest < CompletionTest + def test_command_completion + assert_include(IRB::RegexpCompletor.new.completion_candidates('', 'show_s', '', bind: binding), 'show_source') + assert_not_include(IRB::RegexpCompletor.new.completion_candidates(';', 'show_s', '', bind: binding), 'show_source') + end + end + class MethodCompletionTest < CompletionTest def test_complete_string assert_include(completion_candidates("'foo'.up", binding), "'foo'.upcase") @@ -124,8 +131,9 @@ module TestIRB end def test_complete_require_library_name_first - candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'csv", "", bind: binding) - assert_equal "'csv", candidates.first + # Test that library name is completed first with subdirectories + candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'irb", "", bind: binding) + assert_equal "'irb", candidates.first end def test_complete_require_relative @@ -204,6 +212,13 @@ module TestIRB end end + def test_not_completing_empty_string + assert_equal([], completion_candidates("", binding)) + assert_equal([], completion_candidates(" ", binding)) + assert_equal([], completion_candidates("\t", binding)) + assert_equal(nil, doc_namespace("", binding)) + end + def test_complete_symbol symbols = %w"UTF-16LE UTF-7".map do |enc| "K".force_encoding(enc).to_sym diff --git a/test/irb/test_context.rb b/test/irb/test_context.rb index 79186f13a1..cd3f2c8f62 100644 --- a/test/irb/test_context.rb +++ b/test/irb/test_context.rb @@ -1,7 +1,6 @@ # frozen_string_literal: false require 'tempfile' require 'irb' -require 'rubygems' if defined?(Gem) require_relative "helper" @@ -29,27 +28,6 @@ module TestIRB restore_encodings end - def test_last_value - assert_nil(@context.last_value) - assert_nil(@context.evaluate('_', 1)) - obj = Object.new - @context.set_last_value(obj) - assert_same(obj, @context.last_value) - assert_same(obj, @context.evaluate('_', 1)) - end - - def test_evaluate_with_encoding_error_without_lineno - assert_raise_with_message(EncodingError, /invalid symbol/) { - @context.evaluate(%q[:"\xAE"], 1) - # The backtrace of this invalid encoding hash doesn't contain lineno. - } - end - - def test_evaluate_still_emits_warning - assert_warning("(irb):1: warning: END in method; use at_exit\n") do - @context.evaluate(%q[def foo; END {}; end], 1) - end - end def test_eval_input verbose, $VERBOSE = $VERBOSE, nil @@ -64,11 +42,27 @@ module TestIRB irb.eval_input end assert_empty err - assert_pattern_list([:*, /\(irb\):1:in `<main>': Foo \(RuntimeError\)\n/, - :*, /#<RuntimeError: Foo>\n/, - :*, /0$/, - :*, /0$/, - /\s*/], out) + + expected_output = + if RUBY_3_4 + [ + :*, /\(irb\):1:in '<main>': Foo \(RuntimeError\)\n/, + :*, /#<RuntimeError: Foo>\n/, + :*, /0$/, + :*, /0$/, + /\s*/ + ] + else + [ + :*, /\(irb\):1:in `<main>': Foo \(RuntimeError\)\n/, + :*, /#<RuntimeError: Foo>\n/, + :*, /0$/, + :*, /0$/, + /\s*/ + ] + end + + assert_pattern_list(expected_output, out) ensure $VERBOSE = verbose end @@ -84,11 +78,21 @@ module TestIRB irb.eval_input end assert_empty err - assert_pattern_list([ - :*, /\(irb\):1:in `<main>': Foo \(RuntimeError\)\n/, - :*, /\(irb\):2:in `<main>': Bar \(RuntimeError\)\n/, - :*, /#<RuntimeError: Bar>\n/, - ], out) + expected_output = + if RUBY_3_4 + [ + :*, /\(irb\):1:in '<main>': Foo \(RuntimeError\)\n/, + :*, /\(irb\):2:in '<main>': Bar \(RuntimeError\)\n/, + :*, /#<RuntimeError: Bar>\n/, + ] + else + [ + :*, /\(irb\):1:in `<main>': Foo \(RuntimeError\)\n/, + :*, /\(irb\):2:in `<main>': Bar \(RuntimeError\)\n/, + :*, /#<RuntimeError: Bar>\n/, + ] + end + assert_pattern_list(expected_output, out) end def test_prompt_n_deprecation @@ -126,9 +130,9 @@ module TestIRB [:marshal, "123", Marshal.dump(123)], ], failed: [ - [false, "BasicObject.new", /#<NoMethodError: undefined method `to_s' for/], - [:p, "class Foo; undef inspect ;end; Foo.new", /#<NoMethodError: undefined method `inspect' for/], - [:yaml, "BasicObject.new", /#<NoMethodError: undefined method `inspect' for/], + [false, "BasicObject.new", /#<NoMethodError: undefined method (`|')to_s' for/], + [:p, "class Foo; undef inspect ;end; Foo.new", /#<NoMethodError: undefined method (`|')inspect' for/], + [:yaml, "BasicObject.new", /#<NoMethodError: undefined method (`|')inspect' for/], [:marshal, "[Object.new, Class.new]", /#<TypeError: can't dump anonymous class #<Class:/] ] }.each do |scenario, cases| @@ -206,7 +210,7 @@ module TestIRB end assert_empty err assert_match(/An error occurred when inspecting the object: #<RuntimeError: foo>/, out) - assert_match(/An error occurred when running Kernel#inspect: #<NoMethodError: undefined method `inspect' for/, out) + assert_match(/An error occurred when running Kernel#inspect: #<NoMethodError: undefined method (`|')inspect' for/, out) ensure $VERBOSE = verbose end @@ -349,7 +353,7 @@ module TestIRB end assert_empty err assert_equal("=> \n#{value}\n", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = true @@ -359,7 +363,7 @@ module TestIRB end assert_empty err assert_equal("=> #{value_first_line[0..(input.winsize.last - 9)]}...\n=> \n#{value}\n", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = true @@ -369,7 +373,7 @@ module TestIRB end assert_empty err assert_equal("=> \n#{value}\n=> \n#{value}\n", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = false @@ -379,7 +383,7 @@ module TestIRB end assert_empty err assert_equal("", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = false @@ -389,7 +393,7 @@ module TestIRB end assert_empty err assert_equal("", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = false @@ -399,7 +403,7 @@ module TestIRB end assert_empty err assert_equal("", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) end end @@ -496,22 +500,30 @@ module TestIRB irb.eval_input end assert_empty err - if RUBY_VERSION < '3.0.0' && STDOUT.tty? - expected = [ - :*, /Traceback \(most recent call last\):\n/, - :*, /\t 2: from \(irb\):1:in `<main>'\n/, - :*, /\t 1: from \(irb\):1:in `hoge'\n/, - :*, /\(irb\):1:in `fuga': unhandled exception\n/, - ] - else - expected = [ - :*, /\(irb\):1:in `fuga': unhandled exception\n/, - :*, /\tfrom \(irb\):1:in `hoge'\n/, - :*, /\tfrom \(irb\):1:in `<main>'\n/, - :* - ] - end - assert_pattern_list(expected, out) + expected_output = + if RUBY_3_4 + [ + :*, /\(irb\):1:in 'fuga': unhandled exception\n/, + :*, /\tfrom \(irb\):1:in 'hoge'\n/, + :*, /\tfrom \(irb\):1:in '<main>'\n/, + :* + ] + elsif RUBY_VERSION < '3.0.0' && STDOUT.tty? + [ + :*, /Traceback \(most recent call last\):\n/, + :*, /\t 2: from \(irb\):1:in `<main>'\n/, + :*, /\t 1: from \(irb\):1:in `hoge'\n/, + :*, /\(irb\):1:in `fuga': unhandled exception\n/, + ] + else + [ + :*, /\(irb\):1:in `fuga': unhandled exception\n/, + :*, /\tfrom \(irb\):1:in `hoge'\n/, + :*, /\tfrom \(irb\):1:in `<main>'\n/, + :* + ] + end + assert_pattern_list(expected_output, out) ensure $VERBOSE = verbose end @@ -526,22 +538,31 @@ module TestIRB irb.eval_input end assert_empty err - if RUBY_VERSION < '3.0.0' && STDOUT.tty? - expected = [ - :*, /Traceback \(most recent call last\):\n/, - :*, /\t 2: from \(irb\):1:in `<main>'\n/, - :*, /\t 1: from \(irb\):1:in `hoge'\n/, - :*, /\(irb\):1:in `fuga': A\\xF3B \(RuntimeError\)\n/, - ] - else - expected = [ - :*, /\(irb\):1:in `fuga': A\\xF3B \(RuntimeError\)\n/, - :*, /\tfrom \(irb\):1:in `hoge'\n/, - :*, /\tfrom \(irb\):1:in `<main>'\n/, - :* - ] - end - assert_pattern_list(expected, out) + expected_output = + if RUBY_3_4 + [ + :*, /\(irb\):1:in 'fuga': A\\xF3B \(RuntimeError\)\n/, + :*, /\tfrom \(irb\):1:in 'hoge'\n/, + :*, /\tfrom \(irb\):1:in '<main>'\n/, + :* + ] + elsif RUBY_VERSION < '3.0.0' && STDOUT.tty? + [ + :*, /Traceback \(most recent call last\):\n/, + :*, /\t 2: from \(irb\):1:in `<main>'\n/, + :*, /\t 1: from \(irb\):1:in `hoge'\n/, + :*, /\(irb\):1:in `fuga': A\\xF3B \(RuntimeError\)\n/, + ] + else + [ + :*, /\(irb\):1:in `fuga': A\\xF3B \(RuntimeError\)\n/, + :*, /\tfrom \(irb\):1:in `hoge'\n/, + :*, /\tfrom \(irb\):1:in `<main>'\n/, + :* + ] + end + + assert_pattern_list(expected_output, out) ensure $VERBOSE = verbose end @@ -567,43 +588,43 @@ module TestIRB expected = [ :*, /Traceback \(most recent call last\):\n/, :*, /\t... \d+ levels...\n/, - :*, /\t16: from \(irb\):1:in `a4'\n/, - :*, /\t15: from \(irb\):1:in `a5'\n/, - :*, /\t14: from \(irb\):1:in `a6'\n/, - :*, /\t13: from \(irb\):1:in `a7'\n/, - :*, /\t12: from \(irb\):1:in `a8'\n/, - :*, /\t11: from \(irb\):1:in `a9'\n/, - :*, /\t10: from \(irb\):1:in `a10'\n/, - :*, /\t 9: from \(irb\):1:in `a11'\n/, - :*, /\t 8: from \(irb\):1:in `a12'\n/, - :*, /\t 7: from \(irb\):1:in `a13'\n/, - :*, /\t 6: from \(irb\):1:in `a14'\n/, - :*, /\t 5: from \(irb\):1:in `a15'\n/, - :*, /\t 4: from \(irb\):1:in `a16'\n/, - :*, /\t 3: from \(irb\):1:in `a17'\n/, - :*, /\t 2: from \(irb\):1:in `a18'\n/, - :*, /\t 1: from \(irb\):1:in `a19'\n/, - :*, /\(irb\):1:in `a20': unhandled exception\n/, + :*, /\t16: from \(irb\):1:in (`|')a4'\n/, + :*, /\t15: from \(irb\):1:in (`|')a5'\n/, + :*, /\t14: from \(irb\):1:in (`|')a6'\n/, + :*, /\t13: from \(irb\):1:in (`|')a7'\n/, + :*, /\t12: from \(irb\):1:in (`|')a8'\n/, + :*, /\t11: from \(irb\):1:in (`|')a9'\n/, + :*, /\t10: from \(irb\):1:in (`|')a10'\n/, + :*, /\t 9: from \(irb\):1:in (`|')a11'\n/, + :*, /\t 8: from \(irb\):1:in (`|')a12'\n/, + :*, /\t 7: from \(irb\):1:in (`|')a13'\n/, + :*, /\t 6: from \(irb\):1:in (`|')a14'\n/, + :*, /\t 5: from \(irb\):1:in (`|')a15'\n/, + :*, /\t 4: from \(irb\):1:in (`|')a16'\n/, + :*, /\t 3: from \(irb\):1:in (`|')a17'\n/, + :*, /\t 2: from \(irb\):1:in (`|')a18'\n/, + :*, /\t 1: from \(irb\):1:in (`|')a19'\n/, + :*, /\(irb\):1:in (`|')a20': unhandled exception\n/, ] else expected = [ - :*, /\(irb\):1:in `a20': unhandled exception\n/, - :*, /\tfrom \(irb\):1:in `a19'\n/, - :*, /\tfrom \(irb\):1:in `a18'\n/, - :*, /\tfrom \(irb\):1:in `a17'\n/, - :*, /\tfrom \(irb\):1:in `a16'\n/, - :*, /\tfrom \(irb\):1:in `a15'\n/, - :*, /\tfrom \(irb\):1:in `a14'\n/, - :*, /\tfrom \(irb\):1:in `a13'\n/, - :*, /\tfrom \(irb\):1:in `a12'\n/, - :*, /\tfrom \(irb\):1:in `a11'\n/, - :*, /\tfrom \(irb\):1:in `a10'\n/, - :*, /\tfrom \(irb\):1:in `a9'\n/, - :*, /\tfrom \(irb\):1:in `a8'\n/, - :*, /\tfrom \(irb\):1:in `a7'\n/, - :*, /\tfrom \(irb\):1:in `a6'\n/, - :*, /\tfrom \(irb\):1:in `a5'\n/, - :*, /\tfrom \(irb\):1:in `a4'\n/, + :*, /\(irb\):1:in (`|')a20': unhandled exception\n/, + :*, /\tfrom \(irb\):1:in (`|')a19'\n/, + :*, /\tfrom \(irb\):1:in (`|')a18'\n/, + :*, /\tfrom \(irb\):1:in (`|')a17'\n/, + :*, /\tfrom \(irb\):1:in (`|')a16'\n/, + :*, /\tfrom \(irb\):1:in (`|')a15'\n/, + :*, /\tfrom \(irb\):1:in (`|')a14'\n/, + :*, /\tfrom \(irb\):1:in (`|')a13'\n/, + :*, /\tfrom \(irb\):1:in (`|')a12'\n/, + :*, /\tfrom \(irb\):1:in (`|')a11'\n/, + :*, /\tfrom \(irb\):1:in (`|')a10'\n/, + :*, /\tfrom \(irb\):1:in (`|')a9'\n/, + :*, /\tfrom \(irb\):1:in (`|')a8'\n/, + :*, /\tfrom \(irb\):1:in (`|')a7'\n/, + :*, /\tfrom \(irb\):1:in (`|')a6'\n/, + :*, /\tfrom \(irb\):1:in (`|')a5'\n/, + :*, /\tfrom \(irb\):1:in (`|')a4'\n/, :*, /\t... \d+ levels...\n/, ] end @@ -641,6 +662,14 @@ module TestIRB assert_equal("irb(!ArgumentError)>", irb.send(:format_prompt, 'irb(%M)>', nil, 1, 1)) end + def test_prompt_format + main = 'main' + irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new) + assert_equal('%% main %m %main %%m >', irb.send(:format_prompt, '%%%% %m %%m %%%m %%%%m %l', '>', 1, 1)) + assert_equal('42,%i, 42,%3i,042,%03i', irb.send(:format_prompt, '%i,%%i,%3i,%%3i,%03i,%%03i', nil, 42, 1)) + assert_equal('42,%n, 42,%3n,042,%03n', irb.send(:format_prompt, '%n,%%n,%3n,%%3n,%03n,%%03n', nil, 1, 42)) + end + def test_lineno input = TestInputMethod.new([ "\n", @@ -662,6 +691,18 @@ module TestIRB ], out) end + def test_irb_path_setter + @context.irb_path = __FILE__ + assert_equal(__FILE__, @context.irb_path) + assert_equal("#{__FILE__}(irb)", @context.instance_variable_get(:@eval_path)) + @context.irb_path = 'file/does/not/exist' + assert_equal('file/does/not/exist', @context.irb_path) + assert_equal('file/does/not/exist', @context.instance_variable_get(:@eval_path)) + @context.irb_path = "#{__FILE__}(irb)" + assert_equal("#{__FILE__}(irb)", @context.irb_path) + assert_equal("#{__FILE__}(irb)", @context.instance_variable_get(:@eval_path)) + end + def test_build_completor verbose, $VERBOSE = $VERBOSE, nil original_completor = IRB.conf[:COMPLETOR] diff --git a/test/irb/test_debug_cmd.rb b/test/irb/test_debugger_integration.rb index 0fb45af478..8b1bddea17 100644 --- a/test/irb/test_debug_cmd.rb +++ b/test/irb/test_debugger_integration.rb @@ -6,7 +6,7 @@ require "tmpdir" require_relative "helper" module TestIRB - class DebugCommandTest < IntegrationTestCase + class DebuggerIntegrationTest < IntegrationTestCase def setup super @@ -67,6 +67,22 @@ module TestIRB assert_match(/IRB is already running with a debug session/, output) end + def test_debug_command_can_only_be_called_from_binding_irb + write_ruby <<~'ruby' + require "irb" + # trick test framework + puts "binding.irb" + IRB.start + ruby + + output = run_ruby_file do + type "debug" + type "exit" + end + + assert_include(output, "Debugging commands are only available when IRB is started with binding.irb") + end + def test_next write_ruby <<~'ruby' binding.irb @@ -244,28 +260,46 @@ module TestIRB def test_exit write_ruby <<~'RUBY' binding.irb - puts "hello" + puts "he" + "llo" RUBY output = run_ruby_file do - type "next" + type "debug" type "exit" end - assert_match(/irb\(main\):001> next/, output) + assert_match(/irb:rdbg\(main\):002>/, output) + assert_match(/hello/, output) + end + + def test_force_exit + write_ruby <<~'RUBY' + binding.irb + puts "he" + "llo" + RUBY + + output = run_ruby_file do + type "debug" + type "exit!" + end + + assert_match(/irb:rdbg\(main\):002>/, output) + assert_not_match(/hello/, output) end def test_quit write_ruby <<~'RUBY' binding.irb + puts "he" + "llo" RUBY output = run_ruby_file do - type "next" + type "debug" type "quit!" end - assert_match(/irb\(main\):001> next/, output) + assert_match(/irb:rdbg\(main\):002>/, output) + assert_not_match(/hello/, output) end def test_prompt_line_number_continues @@ -345,19 +379,19 @@ module TestIRB assert_include(output, "### Frame control") end - def test_show_cmds_display_different_content_when_debugger_is_enabled + def test_help_display_different_content_when_debugger_is_enabled write_ruby <<~'ruby' binding.irb ruby output = run_ruby_file do type "debug" - type "show_cmds" + type "help" type "continue" end # IRB's commands should still be listed - assert_match(/show_cmds\s+List all available commands and their description\./, output) + assert_match(/help\s+List all available commands/, output) # debug gem's commands should be appended at the end assert_match(/Debugging \(from debug\.gem\)\s+### Control flow/, output) end @@ -434,5 +468,29 @@ module TestIRB assert_match(/irb\(main\):001> next/, output) assert_include(output, "Multi-IRB commands are not available when the debugger is enabled.") end + + def test_irb_passes_empty_input_to_debugger_to_repeat_the_last_command + write_ruby <<~'ruby' + binding.irb + puts "foo" + puts "bar" + puts "baz" + ruby + + output = run_ruby_file do + type "next" + type "" + # Test that empty input doesn't repeat expressions + type "123" + type "" + type "next" + type "" + type "" + end + + assert_include(output, "=> 2\| puts \"foo\"") + assert_include(output, "=> 3\| puts \"bar\"") + assert_include(output, "=> 4\| puts \"baz\"") + end end end diff --git a/test/irb/test_eval_history.rb b/test/irb/test_eval_history.rb index 0f9ec4811c..54913ceff5 100644 --- a/test/irb/test_eval_history.rb +++ b/test/irb/test_eval_history.rb @@ -30,14 +30,14 @@ module TestIRB end end - def test_eval_history_is_diabled_by_default + def test_eval_history_is_disabled_by_default out, err = execute_lines( "a = 1", "__" ) assert_empty(err) - assert_match(/undefined local variable or method `__'/, out) + assert_match(/undefined local variable or method (`|')__'/, out) end def test_eval_history_can_be_retrieved_with_double_underscore diff --git a/test/irb/test_helper_method.rb b/test/irb/test_helper_method.rb new file mode 100644 index 0000000000..291278c16a --- /dev/null +++ b/test/irb/test_helper_method.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true +require "irb" + +require_relative "helper" + +module TestIRB + class HelperMethodTestCase < TestCase + def setup + $VERBOSE = nil + @verbosity = $VERBOSE + save_encodings + IRB.instance_variable_get(:@CONF).clear + end + + def teardown + $VERBOSE = @verbosity + restore_encodings + end + + def execute_lines(*lines, conf: {}, main: self, irb_path: nil) + IRB.init_config(nil) + IRB.conf[:VERBOSE] = false + IRB.conf[:PROMPT_MODE] = :SIMPLE + IRB.conf.merge!(conf) + input = TestInputMethod.new(lines) + irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) + irb.context.return_format = "=> %s\n" + irb.context.irb_path = irb_path if irb_path + IRB.conf[:MAIN_CONTEXT] = irb.context + IRB.conf[:USE_PAGER] = false + capture_output do + irb.eval_input + end + end + end + + module TestHelperMethod + class ConfTest < HelperMethodTestCase + def test_conf_returns_the_context_object + out, err = execute_lines("conf.ap_name") + + assert_empty err + assert_include out, "=> \"irb\"" + end + end + end + + class HelperMethodIntegrationTest < IntegrationTestCase + def test_arguments_propogation + write_ruby <<~RUBY + require "irb/helper_method" + + class MyHelper < IRB::HelperMethod::Base + description "This is a test helper" + + def execute( + required_arg, optional_arg = nil, *splat_arg, required_keyword_arg:, + optional_keyword_arg: nil, **double_splat_arg, &block_arg + ) + puts [required_arg, optional_arg, splat_arg, required_keyword_arg, optional_keyword_arg, double_splat_arg, block_arg.call].to_s + end + end + + IRB::HelperMethod.register(:my_helper, MyHelper) + + binding.irb + RUBY + + output = run_ruby_file do + type <<~INPUT + my_helper( + "required", "optional", "splat", required_keyword_arg: "required", + optional_keyword_arg: "optional", a: 1, b: 2 + ) { "block" } + INPUT + type "exit" + end + + assert_include(output, '["required", "optional", ["splat"], "required", "optional", {:a=>1, :b=>2}, "block"]') + end + + def test_helper_method_injection_can_happen_after_irb_require + write_ruby <<~RUBY + require "irb" + + class MyHelper < IRB::HelperMethod::Base + description "This is a test helper" + + def execute + puts "Hello from MyHelper" + end + end + + IRB::HelperMethod.register(:my_helper, MyHelper) + + binding.irb + RUBY + + output = run_ruby_file do + type "my_helper" + type "exit" + end + + assert_include(output, 'Hello from MyHelper') + end + + def test_helper_method_instances_are_memoized + write_ruby <<~RUBY + require "irb/helper_method" + + class MyHelper < IRB::HelperMethod::Base + description "This is a test helper" + + def execute(val) + @val ||= val + end + end + + IRB::HelperMethod.register(:my_helper, MyHelper) + + binding.irb + RUBY + + output = run_ruby_file do + type "my_helper(100)" + type "my_helper(200)" + type "exit" + end + + assert_include(output, '=> 100') + assert_not_include(output, '=> 200') + end + end +end diff --git a/test/irb/test_history.rb b/test/irb/test_history.rb index f7ba2b9d33..63be35fdaa 100644 --- a/test/irb/test_history.rb +++ b/test/irb/test_history.rb @@ -10,11 +10,24 @@ return if RUBY_PLATFORM.match?(/solaris|mswin|mingw/i) module TestIRB class HistoryTest < TestCase def setup - IRB.conf[:RC_NAME_GENERATOR] = nil + @original_verbose, $VERBOSE = $VERBOSE, nil + @tmpdir = Dir.mktmpdir("test_irb_history_") + @backup_home = ENV["HOME"] + @backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") + @backup_irbrc = ENV.delete("IRBRC") + @backup_default_external = Encoding.default_external + ENV["HOME"] = @tmpdir + IRB.instance_variable_set(:@existing_rc_name_generators, nil) end def teardown - IRB.conf[:RC_NAME_GENERATOR] = nil + IRB.instance_variable_set(:@existing_rc_name_generators, nil) + ENV["HOME"] = @backup_home + ENV["XDG_CONFIG_HOME"] = @backup_xdg_config_home + ENV["IRBRC"] = @backup_irbrc + Encoding.default_external = @backup_default_external + $VERBOSE = @original_verbose + FileUtils.rm_rf(@tmpdir) end class TestInputMethodWithRelineHistory < TestInputMethod @@ -123,35 +136,23 @@ module TestIRB end def test_history_concurrent_use_not_present - backup_home = ENV["HOME"] - backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") - backup_irbrc = ENV.delete("IRBRC") IRB.conf[:LC_MESSAGES] = IRB::Locale.new IRB.conf[:SAVE_HISTORY] = 1 - Dir.mktmpdir("test_irb_history_") do |tmpdir| - ENV["HOME"] = tmpdir - io = TestInputMethodWithRelineHistory.new - io.class::HISTORY.clear - io.load_history - io.class::HISTORY << 'line1' - io.class::HISTORY << 'line2' - - history_file = IRB.rc_file("_history") - assert_not_send [File, :file?, history_file] - File.write(history_file, "line0\n") - io.save_history - assert_equal(%w"line0 line1 line2", File.read(history_file).split) - end - ensure - ENV["HOME"] = backup_home - ENV["XDG_CONFIG_HOME"] = backup_xdg_config_home - ENV["IRBRC"] = backup_irbrc + io = TestInputMethodWithRelineHistory.new + io.class::HISTORY.clear + io.load_history + io.class::HISTORY << 'line1' + io.class::HISTORY << 'line2' + + history_file = IRB.rc_file("_history") + assert_not_send [File, :file?, history_file] + File.write(history_file, "line0\n") + io.save_history + assert_equal(%w"line0 line1 line2", File.read(history_file).split) end def test_history_different_encodings - backup_default_external = Encoding.default_external IRB.conf[:SAVE_HISTORY] = 2 - verbose_bak, $VERBOSE = $VERBOSE, nil Encoding.default_external = Encoding::US_ASCII locale = IRB::Locale.new("C") assert_history(<<~EXPECTED_HISTORY.encode(Encoding::US_ASCII), <<~INITIAL_HISTORY.encode(Encoding::UTF_8), <<~INPUT, locale: locale) @@ -162,9 +163,34 @@ module TestIRB INITIAL_HISTORY exit INPUT + end + + def test_history_does_not_raise_when_history_file_directory_does_not_exist + backup_history_file = IRB.conf[:HISTORY_FILE] + IRB.conf[:SAVE_HISTORY] = 1 + IRB.conf[:HISTORY_FILE] = "fake/fake/fake/history_file" + io = TestInputMethodWithRelineHistory.new + + assert_warn(/history file does not exist/) do + io.save_history + end + + # assert_warn reverts $VERBOSE to EnvUtil.original_verbose, which is true in some cases + # We want to keep $VERBOSE as nil until teardown is called + # TODO: check if this is an assert_warn issue + $VERBOSE = nil ensure - Encoding.default_external = backup_default_external - $VERBOSE = verbose_bak + IRB.conf[:HISTORY_FILE] = backup_history_file + end + + def test_no_home_no_history_file_does_not_raise_history_save + ENV['HOME'] = nil + io = TestInputMethodWithRelineHistory.new + assert_nil(IRB.rc_file('_history')) + assert_nothing_raised do + io.load_history + io.save_history + end end private @@ -199,34 +225,30 @@ module TestIRB end def assert_history(expected_history, initial_irb_history, input, input_method = TestInputMethodWithRelineHistory, locale: IRB::Locale.new) - backup_verbose, $VERBOSE = $VERBOSE, nil - backup_home = ENV["HOME"] - backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") IRB.conf[:LC_MESSAGES] = locale actual_history = nil - Dir.mktmpdir("test_irb_history_") do |tmpdir| - ENV["HOME"] = tmpdir - File.open(IRB.rc_file("_history"), "w") do |f| - f.write(initial_irb_history) - end + history_file = IRB.rc_file("_history") + ENV["HOME"] = @tmpdir + File.open(history_file, "w") do |f| + f.write(initial_irb_history) + end - io = input_method.new + io = input_method.new + io.class::HISTORY.clear + io.load_history + if block_given? + previous_history = [] + io.class::HISTORY.each { |line| previous_history << line } + yield history_file io.class::HISTORY.clear - io.load_history - if block_given? - previous_history = [] - io.class::HISTORY.each { |line| previous_history << line } - yield IRB.rc_file("_history") - io.class::HISTORY.clear - previous_history.each { |line| io.class::HISTORY << line } - end - input.split.each { |line| io.class::HISTORY << line } - io.save_history + previous_history.each { |line| io.class::HISTORY << line } + end + input.split.each { |line| io.class::HISTORY << line } + io.save_history - io.load_history - File.open(IRB.rc_file("_history"), "r") do |f| - actual_history = f.read - end + io.load_history + File.open(history_file, "r") do |f| + actual_history = f.read end assert_equal(expected_history, actual_history, <<~MESSAGE) expected: @@ -234,10 +256,6 @@ module TestIRB but actual: #{actual_history} MESSAGE - ensure - $VERBOSE = backup_verbose - ENV["HOME"] = backup_home - ENV["XDG_CONFIG_HOME"] = backup_xdg_config_home end def with_temp_stdio @@ -251,10 +269,6 @@ module TestIRB class IRBHistoryIntegrationTest < IntegrationTestCase def test_history_saving_with_debug - if ruby_core? - omit "This test works only under ruby/irb" - end - write_history "" write_ruby <<~'RUBY' @@ -293,6 +307,53 @@ module TestIRB HISTORY end + def test_history_saving_with_debug_without_prior_history + tmpdir = Dir.mktmpdir("test_irb_history_") + # Intentionally not creating the file so we test the reset counter logic + history_file = File.join(tmpdir, "irb_history") + + write_rc <<~RUBY + IRB.conf[:HISTORY_FILE] = "#{history_file}" + RUBY + + write_ruby <<~'RUBY' + def foo + end + + binding.irb + + foo + RUBY + + output = run_ruby_file do + type "'irb session'" + type "next" + type "'irb:debug session'" + type "step" + type "irb_info" + type "puts Reline::HISTORY.to_a.to_s" + type "q!" + end + + assert_include(output, "InputMethod: RelineInputMethod") + # check that in-memory history is preserved across sessions + assert_include output, %q( + ["'irb session'", "next", "'irb:debug session'", "step", "irb_info", "puts Reline::HISTORY.to_a.to_s"] + ).strip + + assert_equal <<~HISTORY, File.read(history_file) + 'irb session' + next + 'irb:debug session' + step + irb_info + puts Reline::HISTORY.to_a.to_s + q! + HISTORY + ensure + FileUtils.rm_rf(tmpdir) + end + def test_history_saving_with_nested_sessions write_history "" @@ -323,6 +384,62 @@ module TestIRB HISTORY end + def test_nested_history_saving_from_inner_session_with_exit! + write_history "" + + write_ruby <<~'RUBY' + def foo + binding.irb + end + + binding.irb + RUBY + + run_ruby_file do + type "'outer session'" + type "foo" + type "'inner session'" + type "exit!" + end + + assert_equal <<~HISTORY, @history_file.open.read + 'outer session' + foo + 'inner session' + exit! + HISTORY + end + + def test_nested_history_saving_from_outer_session_with_exit! + write_history "" + + write_ruby <<~'RUBY' + def foo + binding.irb + end + + binding.irb + RUBY + + run_ruby_file do + type "'outer session'" + type "foo" + type "'inner session'" + type "exit" + type "'outer session again'" + type "exit!" + end + + assert_equal <<~HISTORY, @history_file.open.read + 'outer session' + foo + 'inner session' + exit + 'outer session again' + exit! + HISTORY + end + def test_history_saving_with_nested_sessions_and_prior_history write_history <<~HISTORY old_history_1 diff --git a/test/irb/test_init.rb b/test/irb/test_init.rb index 7d11b5b507..f11d7398c8 100644 --- a/test/irb/test_init.rb +++ b/test/irb/test_init.rb @@ -11,13 +11,18 @@ module TestIRB @backup_env = %w[HOME XDG_CONFIG_HOME IRBRC].each_with_object({}) do |env, hash| hash[env] = ENV.delete(env) end - ENV["HOME"] = @tmpdir = Dir.mktmpdir("test_irb_init_#{$$}") + ENV["HOME"] = @tmpdir = File.realpath(Dir.mktmpdir("test_irb_init_#{$$}")) + end + + def reset_rc_name_generators + IRB.instance_variable_set(:@existing_rc_name_generators, nil) end def teardown ENV.update(@backup_env) FileUtils.rm_rf(@tmpdir) IRB.conf.delete(:SCRIPT) + reset_rc_name_generators end def test_setup_with_argv_preserves_global_argv @@ -34,42 +39,80 @@ module TestIRB assert_equal orig, $0 end - def test_rc_file + def test_rc_files tmpdir = @tmpdir Dir.chdir(tmpdir) do - ENV["XDG_CONFIG_HOME"] = "#{tmpdir}/xdg" - IRB.conf[:RC_NAME_GENERATOR] = nil - assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) - assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) - assert_file.not_exist?(tmpdir+"/xdg") - IRB.conf[:RC_NAME_GENERATOR] = nil - FileUtils.touch(tmpdir+"/.irb#{IRB::IRBRC_EXT}") - assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) - assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) - assert_file.not_exist?(tmpdir+"/xdg") + home = ENV['HOME'] = "#{tmpdir}/home" + xdg_config_home = ENV['XDG_CONFIG_HOME'] = "#{tmpdir}/xdg" + reset_rc_name_generators + assert_empty(IRB.irbrc_files) + assert_equal("#{home}/.irb_history", IRB.rc_file('_history')) + FileUtils.mkdir_p(home) + FileUtils.mkdir_p("#{xdg_config_home}/irb") + FileUtils.mkdir_p("#{home}/.config/irb") + reset_rc_name_generators + assert_empty(IRB.irbrc_files) + assert_equal("#{xdg_config_home}/irb/irb_history", IRB.rc_file('_history')) + home_irbrc = "#{home}/.irbrc" + config_irbrc = "#{home}/.config/irb/irbrc" + xdg_config_irbrc = "#{xdg_config_home}/irb/irbrc" + [home_irbrc, config_irbrc, xdg_config_irbrc].each do |file| + FileUtils.touch(file) + end + current_dir_irbrcs = %w[.irbrc irbrc _irbrc $irbrc].map { |file| "#{tmpdir}/#{file}" } + current_dir_irbrcs.each { |file| FileUtils.touch(file) } + reset_rc_name_generators + assert_equal([xdg_config_irbrc, home_irbrc, *current_dir_irbrcs], IRB.irbrc_files) + assert_equal(xdg_config_irbrc.sub(/rc$/, '_history'), IRB.rc_file('_history')) + ENV['XDG_CONFIG_HOME'] = nil + reset_rc_name_generators + assert_equal([home_irbrc, config_irbrc, *current_dir_irbrcs], IRB.irbrc_files) + assert_equal(home_irbrc.sub(/rc$/, '_history'), IRB.rc_file('_history')) + ENV['XDG_CONFIG_HOME'] = '' + reset_rc_name_generators + assert_equal([home_irbrc, config_irbrc] + current_dir_irbrcs, IRB.irbrc_files) + assert_equal(home_irbrc.sub(/rc$/, '_history'), IRB.rc_file('_history')) + ENV['XDG_CONFIG_HOME'] = xdg_config_home + ENV['IRBRC'] = "#{tmpdir}/.irbrc" + reset_rc_name_generators + assert_equal([ENV['IRBRC'], xdg_config_irbrc, home_irbrc] + (current_dir_irbrcs - [ENV['IRBRC']]), IRB.irbrc_files) + assert_equal(ENV['IRBRC'] + '_history', IRB.rc_file('_history')) + ENV['IRBRC'] = ENV['HOME'] = ENV['XDG_CONFIG_HOME'] = nil + reset_rc_name_generators + assert_equal(current_dir_irbrcs, IRB.irbrc_files) + assert_nil(IRB.rc_file('_history')) end end - def test_rc_file_in_subdir + def test_duplicated_rc_files tmpdir = @tmpdir Dir.chdir(tmpdir) do - FileUtils.mkdir_p("#{tmpdir}/mydir") - Dir.chdir("#{tmpdir}/mydir") do - IRB.conf[:RC_NAME_GENERATOR] = nil - assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) - assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) - IRB.conf[:RC_NAME_GENERATOR] = nil - FileUtils.touch(tmpdir+"/.irb#{IRB::IRBRC_EXT}") - assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) - assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) + ENV['XDG_CONFIG_HOME'] = "#{ENV['HOME']}/.config" + FileUtils.mkdir_p("#{ENV['XDG_CONFIG_HOME']}/irb") + env_irbrc = ENV['IRBRC'] = "#{tmpdir}/_irbrc" + xdg_config_irbrc = "#{ENV['XDG_CONFIG_HOME']}/irb/irbrc" + home_irbrc = "#{ENV['HOME']}/.irbrc" + current_dir_irbrc = "#{tmpdir}/irbrc" + [env_irbrc, xdg_config_irbrc, home_irbrc, current_dir_irbrc].each do |file| + FileUtils.touch(file) end + reset_rc_name_generators + assert_equal([env_irbrc, xdg_config_irbrc, home_irbrc, current_dir_irbrc], IRB.irbrc_files) end end - def test_recovery_sigint + def test_sigint_restore_default pend "This test gets stuck on Solaris for unknown reason; contribution is welcome" if RUBY_PLATFORM =~ /solaris/ bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] - status = assert_in_out_err(bundle_exec + %w[-W0 -rirb -e binding.irb;loop{Process.kill("SIGINT",$$)} -- -f --], "exit\n", //, //) + # IRB should restore SIGINT handler + status = assert_in_out_err(bundle_exec + %w[-W0 -rirb -e Signal.trap("SIGINT","DEFAULT");binding.irb;loop{Process.kill("SIGINT",$$)} -- -f --], "exit\n", //, //) + Process.kill("SIGKILL", status.pid) if !status.exited? && !status.stopped? && !status.signaled? + end + + def test_sigint_restore_block + bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] + # IRB should restore SIGINT handler + status = assert_in_out_err(bundle_exec + %w[-W0 -rirb -e x=false;Signal.trap("SIGINT"){x=true};binding.irb;loop{Process.kill("SIGINT",$$);if(x);break;end} -- -f --], "exit\n", //, //) Process.kill("SIGKILL", status.pid) if !status.exited? && !status.stopped? && !status.signaled? end @@ -208,6 +251,12 @@ module TestIRB assert_equal(['-f'], argv) end + def test_option_tracer + argv = %w[--tracer] + IRB.setup(eval("__FILE__"), argv: argv) + assert_equal(true, IRB.conf[:USE_TRACER]) + end + private def with_argv(argv) diff --git a/test/irb/test_input_method.rb b/test/irb/test_input_method.rb index e6a1b06e82..ce317b4b32 100644 --- a/test/irb/test_input_method.rb +++ b/test/irb/test_input_method.rb @@ -15,7 +15,7 @@ module TestIRB def teardown IRB.conf.replace(@conf_backup) restore_encodings - # Reset Reline configuration overrided by RelineInputMethod. + # Reset Reline configuration overridden by RelineInputMethod. Reline.instance_variable_set(:@core, nil) end end @@ -88,17 +88,18 @@ module TestIRB @driver = RDoc::RI::Driver.new(use_stdout: true) end - def display_document(target, bind) + def display_document(target, bind, driver = nil) input_method = IRB::RelineInputMethod.new(IRB::RegexpCompletor.new) - input_method.instance_variable_set(:@completion_params, [target, '', '', bind]) - input_method.display_document(target, driver: @driver) + input_method.instance_variable_set(:@rdoc_ri_driver, driver) if driver + input_method.instance_variable_set(:@completion_params, ['', target, '', bind]) + input_method.display_document(target) end def test_perfectly_matched_namespace_triggers_document_display omit unless has_rdoc_content? out, err = capture_output do - display_document("String", binding) + display_document("String", binding, @driver) end assert_empty(err) @@ -109,7 +110,7 @@ module TestIRB def test_perfectly_matched_multiple_namespaces_triggers_document_display result = nil out, err = capture_output do - result = display_document("{}.nil?", binding) + result = display_document("{}.nil?", binding, @driver) end assert_empty(err) @@ -131,7 +132,7 @@ module TestIRB def test_not_matched_namespace_triggers_nothing result = nil out, err = capture_output do - result = display_document("Stri", binding) + result = display_document("Stri", binding, @driver) end assert_empty(err) @@ -156,7 +157,7 @@ module TestIRB def test_perfect_matching_handles_nil_namespace out, err = capture_output do # symbol literal has `nil` doc namespace so it's a good test subject - assert_nil(display_document(":aiueo", binding)) + assert_nil(display_document(":aiueo", binding, @driver)) end assert_empty(err) @@ -170,4 +171,3 @@ module TestIRB end end end - diff --git a/test/irb/test_irb.rb b/test/irb/test_irb.rb index fb8b5c2bfa..28be744088 100644 --- a/test/irb/test_irb.rb +++ b/test/irb/test_irb.rb @@ -42,6 +42,56 @@ module TestIRB assert_include output, "From: #{@ruby_file.path}:1" end + def test_underscore_stores_last_result + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type "1 + 1" + type "_ + 10" + type "exit!" + end + + assert_include output, "=> 12" + end + + def test_evaluate_with_encoding_error_without_lineno + if RUBY_ENGINE == 'truffleruby' + omit "Remove me after https://github.com/ruby/prism/issues/2129 is addressed and adopted in TruffleRuby" + end + + if RUBY_VERSION >= "3.4." + omit "Now raises SyntaxError" + end + + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type %q[:"\xAE"] + type "exit!" + end + + assert_include output, 'invalid symbol in encoding UTF-8 :"\xAE"' + # EncodingError would be wrapped with ANSI escape sequences, so we assert it separately + assert_include output, "EncodingError" + end + + def test_evaluate_still_emits_warning + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type %q[def foo; END {}; end] + type "exit!" + end + + assert_include output, '(irb):1: warning: END in method; use at_exit' + end + def test_symbol_aliases_dont_affect_ruby_syntax write_ruby <<~'RUBY' $foo = "It's a foo" @@ -58,6 +108,41 @@ module TestIRB assert_include output, "=> \"It's a foo\"" assert_include output, "=> \"It's a bar\"" end + + def test_empty_input_echoing_behaviour + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type "" + type " " + type "exit" + end + + assert_not_match(/irb\(main\):001> (\r*\n)?=> nil/, output) + assert_match(/irb\(main\):002> (\r*\n)?=> nil/, output) + end + end + + class NestedBindingIrbTest < IntegrationTestCase + def test_current_context_restore + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type '$ctx = IRB.CurrentContext' + type 'binding.irb' + type 'p context_changed: IRB.CurrentContext != $ctx' + type 'exit' + type 'p context_restored: IRB.CurrentContext == $ctx' + type 'exit' + end + + assert_include output, '{:context_changed=>true}' + assert_include output, '{:context_restored=>true}' + end end class IrbIOConfigurationTest < TestCase @@ -738,4 +823,95 @@ module TestIRB IRB::Irb.new(workspace, TestInputMethod.new) end end + + class BacktraceFilteringTest < TestIRB::IntegrationTestCase + def test_backtrace_filtering + write_ruby <<~'RUBY' + def foo + raise "error" + end + + def bar + foo + end + + binding.irb + RUBY + + output = run_ruby_file do + type "bar" + type "exit" + end + + assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output) + frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip) + + expected_traces = if RUBY_VERSION >= "3.3.0" + [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/, + /from <internal:kernel>:\d+:in (`|'Kernel#)loop'/, + /from <internal:prelude>:\d+:in (`|'Binding#)irb'/, + /from .*\/irbtest-.*.rb:9:in [`']<main>'/ + ] + else + [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/, + /from <internal:prelude>:\d+:in (`|'Binding#)irb'/, + /from .*\/irbtest-.*.rb:9:in [`']<main>'/ + ] + end + + expected_traces.reverse! if RUBY_VERSION < "3.0.0" + + expected_traces.each_with_index do |expected_trace, index| + assert_match(expected_trace, frame_traces[index]) + end + end + + def test_backtrace_filtering_with_backtrace_filter + write_rc <<~'RUBY' + class TestBacktraceFilter + def self.call(backtrace) + backtrace.reject { |line| line.include?("internal") } + end + end + + IRB.conf[:BACKTRACE_FILTER] = TestBacktraceFilter + RUBY + + write_ruby <<~'RUBY' + def foo + raise "error" + end + + def bar + foo + end + + binding.irb + RUBY + + output = run_ruby_file do + type "bar" + type "exit" + end + + assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output) + frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip) + + expected_traces = [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/, + /from .*\/irbtest-.*.rb:9:in [`']<main>'/ + ] + + expected_traces.reverse! if RUBY_VERSION < "3.0.0" + + expected_traces.each_with_index do |expected_trace, index| + assert_match(expected_trace, frame_traces[index]) + end + end + end end diff --git a/test/irb/test_nesting_parser.rb b/test/irb/test_nesting_parser.rb index ea3a23aaf5..2482d40081 100644 --- a/test/irb/test_nesting_parser.rb +++ b/test/irb/test_nesting_parser.rb @@ -280,6 +280,44 @@ module TestIRB end end + def test_undef_alias + codes = [ + 'undef foo', + 'alias foo bar', + 'undef !', + 'alias + -', + 'alias $a $b', + 'undef do', + 'alias do do', + 'undef :do', + 'alias :do :do', + 'undef :"#{alias do do}"', + 'alias :"#{undef do}" do', + 'alias do :"#{undef do}"' + ] + code_with_comment = <<~EOS + undef # + # + do # + alias # + # + do # + # + do # + EOS + code_with_heredoc = <<~EOS + <<~A; alias + A + :"#{<<~A}" + A + do + EOS + [*codes, code_with_comment, code_with_heredoc].each do |code| + opens = IRB::NestingParser.open_tokens(IRB::RubyLex.ripper_lex_without_warning('(' + code + "\nif")) + assert_equal(%w[( if], opens.map(&:tok)) + end + end + def test_case_in if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0') pend 'This test requires ruby version that supports case-in syntax' diff --git a/test/irb/test_raise_exception.rb b/test/irb/test_raise_exception.rb index c373dd7335..44a5ae87e1 100644 --- a/test/irb/test_raise_exception.rb +++ b/test/irb/test_raise_exception.rb @@ -61,7 +61,7 @@ IRB # TruffleRuby warns when the locale does not exist env['TRUFFLERUBYOPT'] = "#{ENV['TRUFFLERUBYOPT']} --log.level=SEVERE" if RUBY_ENGINE == 'truffleruby' args = [env] + bundle_exec + %W[-rirb -C #{tmpdir} -W0 -e IRB.start(__FILE__) -- -f --] - error = /`raise_euc_with_invalid_byte_sequence': あ\\xFF \(RuntimeError\)/ + error = /raise_euc_with_invalid_byte_sequence': あ\\xFF \(RuntimeError\)/ assert_in_out_err(args, <<~IRB, error, [], encoding: "UTF-8") require_relative 'euc' raise_euc_with_invalid_byte_sequence diff --git a/test/irb/test_ruby_lex.rb b/test/irb/test_ruby_lex.rb index 5cfd81dbe8..4e406a8ce0 100644 --- a/test/irb/test_ruby_lex.rb +++ b/test/irb/test_ruby_lex.rb @@ -221,6 +221,10 @@ module TestIRB end def assert_code_block_open(lines, expected, local_variables: []) + if RUBY_ENGINE == 'truffleruby' + omit "Remove me after https://github.com/ruby/prism/issues/2129 is addressed and adopted in TruffleRuby" + end + _indent_level, _continue, code_block_open = check_state(lines, local_variables: local_variables) error_message = "Wrong result of code_block_open for:\n #{lines.join("\n")}" assert_equal(expected, code_block_open, error_message) diff --git a/test/irb/test_tracer.rb b/test/irb/test_tracer.rb new file mode 100644 index 0000000000..540f8be131 --- /dev/null +++ b/test/irb/test_tracer.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: false +require 'tempfile' +require 'irb' + +require_relative "helper" + +module TestIRB + class ContextWithTracerIntegrationTest < IntegrationTestCase + def setup + super + + omit "Tracer gem is not available when running on TruffleRuby" if RUBY_ENGINE == "truffleruby" + + @envs.merge!("NO_COLOR" => "true") + end + + def example_ruby_file + <<~'RUBY' + class Foo + def self.foo + 100 + end + end + + def bar(obj) + obj.foo + end + + binding.irb + RUBY + end + + def test_use_tracer_enabled_when_gem_is_unavailable + write_rc <<~RUBY + # Simulate the absence of the tracer gem + ::Kernel.send(:alias_method, :irb_original_require, :require) + + ::Kernel.define_method(:require) do |name| + raise LoadError, "cannot load such file -- tracer (test)" if name.match?("tracer") + ::Kernel.send(:irb_original_require, name) + end + + IRB.conf[:USE_TRACER] = true + RUBY + + write_ruby example_ruby_file + + output = run_ruby_file do + type "bar(Foo)" + type "exit" + end + + assert_include(output, "Tracer extension of IRB is enabled but tracer gem wasn't found.") + end + + def test_use_tracer_enabled_when_gem_is_available + write_rc <<~RUBY + IRB.conf[:USE_TRACER] = true + RUBY + + write_ruby example_ruby_file + + output = run_ruby_file do + type "bar(Foo)" + type "exit" + end + + assert_include(output, "Object#bar at") + assert_include(output, "Foo.foo at") + assert_include(output, "Foo.foo #=> 100") + assert_include(output, "Object#bar #=> 100") + + # Test that the tracer output does not include IRB's own files + assert_not_include(output, "irb/workspace.rb") + end + + def test_use_tracer_is_disabled_by_default + write_ruby example_ruby_file + + output = run_ruby_file do + type "bar(Foo)" + type "exit" + end + + assert_not_include(output, "#depth:") + assert_not_include(output, "Foo.foo") + end + + end +end diff --git a/test/irb/test_type_completor.rb b/test/irb/test_type_completor.rb index cf4fc12c9f..5ed8988b34 100644 --- a/test/irb/test_type_completor.rb +++ b/test/irb/test_type_completor.rb @@ -9,7 +9,7 @@ rescue LoadError return end -require 'irb/completion' +require 'irb' require 'tempfile' require_relative './helper' @@ -54,6 +54,11 @@ module TestIRB assert_equal [], candidates assert_doc_namespace('(', ')', nil) end + + def test_command_completion + assert_include(@completor.completion_candidates('', 'show_s', '', bind: binding), 'show_source') + assert_not_include(@completor.completion_candidates(';', 'show_s', '', bind: binding), 'show_source') + end end class TypeCompletorIntegrationTest < IntegrationTestCase diff --git a/test/irb/test_workspace.rb b/test/irb/test_workspace.rb index ff17b1a22c..199ce95a37 100644 --- a/test/irb/test_workspace.rb +++ b/test/irb/test_workspace.rb @@ -1,6 +1,5 @@ # frozen_string_literal: false require 'tempfile' -require 'rubygems' require 'irb' require 'irb/workspace' require 'irb/color' @@ -91,7 +90,7 @@ module TestIRB irb_path = "#{top_srcdir}/#{dir}/irb" File.exist?(irb_path) end or omit 'irb command not found' - assert_in_out_err(bundle_exec + ['-W0', "-C#{top_srcdir}", '-e', <<~RUBY , '--', '-f', '--'], 'binding.local_variables', /\[:_\]/, [], bug17623) + assert_in_out_err(bundle_exec + ['-W0', "-C#{top_srcdir}", '-e', <<~RUBY, '--', '-f', '--'], 'binding.local_variables', /\[:_\]/, [], bug17623) version = 'xyz' # typical rubygems loading file load('#{irb_path}') RUBY diff --git a/test/irb/yamatanooroti/test_rendering.rb b/test/irb/yamatanooroti/test_rendering.rb index b18b95bbbd..44e07a3a12 100644 --- a/test/irb/yamatanooroti/test_rendering.rb +++ b/test/irb/yamatanooroti/test_rendering.rb @@ -11,6 +11,8 @@ end class IRB::RenderingTest < Yamatanooroti::TestCase def setup @original_term = ENV['TERM'] + @home_backup = ENV['HOME'] + @xdg_config_home_backup = ENV['XDG_CONFIG_HOME'] ENV['TERM'] = "xterm-256color" @pwd = Dir.pwd suffix = '%010d' % Random.rand(0..65535) @@ -24,13 +26,16 @@ class IRB::RenderingTest < Yamatanooroti::TestCase @irbrc_backup = ENV['IRBRC'] @irbrc_file = ENV['IRBRC'] = File.join(@tmpdir, 'temporaty_irbrc') File.unlink(@irbrc_file) if File.exist?(@irbrc_file) + ENV['HOME'] = File.join(@tmpdir, 'home') + ENV['XDG_CONFIG_HOME'] = File.join(@tmpdir, 'xdg_config_home') end def teardown FileUtils.rm_rf(@tmpdir) ENV['IRBRC'] = @irbrc_backup ENV['TERM'] = @original_term - ENV.delete('RELINE_TEST_PROMPT') if ENV['RELINE_TEST_PROMPT'] + ENV['HOME'] = @home_backup + ENV['XDG_CONFIG_HOME'] = @xdg_config_home_backup end def test_launch @@ -50,6 +55,42 @@ class IRB::RenderingTest < Yamatanooroti::TestCase EOC end + def test_configuration_file_is_skipped_with_dash_f + write_irbrc <<~'LINES' + puts '.irbrc file should be ignored when -f is used' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb -f}, startup_message: '') + write(<<~EOC) + 'Hello, World!' + EOC + close + assert_screen(<<~EOC) + irb(main):001> 'Hello, World!' + => "Hello, World!" + irb(main):002> + EOC + end + + def test_configuration_file_is_skipped_with_dash_f_for_nested_sessions + write_irbrc <<~'LINES' + puts '.irbrc file should be ignored when -f is used' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb -f}, startup_message: '') + write(<<~EOC) + 'Hello, World!' + binding.irb + exit! + EOC + close + assert_screen(<<~EOC) + irb(main):001> 'Hello, World!' + => "Hello, World!" + irb(main):002> binding.irb + irb(main):003> exit! + irb(main):001> + EOC + end + def test_nomultiline write_irbrc <<~'LINES' puts 'start IRB' @@ -96,6 +137,7 @@ class IRB::RenderingTest < Yamatanooroti::TestCase a .a .b + .itself EOC close assert_screen(<<~EOC) @@ -112,9 +154,10 @@ class IRB::RenderingTest < Yamatanooroti::TestCase irb(main):008> irb(main):009> a irb(main):010> .a - irb(main):011> .b + irb(main):011> .b + irb(main):012> .itself => true - irb(main):012> + irb(main):013> EOC end @@ -140,7 +183,6 @@ class IRB::RenderingTest < Yamatanooroti::TestCase (a) &.b() - class A def b; self; end; def c; true; end; end; a = A.new a @@ -149,6 +191,7 @@ class IRB::RenderingTest < Yamatanooroti::TestCase .c (a) &.b() + .itself EOC close assert_screen(<<~EOC) @@ -173,17 +216,17 @@ class IRB::RenderingTest < Yamatanooroti::TestCase irb(main):015> &.b() => #<A> irb(main):016> - irb(main):017> - irb(main):018> class A def b; self; end; def c; true; end; end; - irb(main):019> a = A.new + irb(main):017> class A def b; self; end; def c; true; end; end; + irb(main):018> a = A.new => #<A> - irb(main):020> a - irb(main):021> .b - irb(main):022> # aaa - irb(main):023> .c + irb(main):019> a + irb(main):020> .b + irb(main):021> # aaa + irb(main):022> .c => true - irb(main):024> (a) - irb(main):025> &.b() + irb(main):023> (a) + irb(main):024> &.b() + irb(main):025> .itself => #<A> irb(main):026> EOC @@ -213,9 +256,9 @@ class IRB::RenderingTest < Yamatanooroti::TestCase start_terminal(3, 50, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') write("{}.__id_") write("\C-i") + sleep 0.2 close screen = result.join("\n").sub(/\n*\z/, "\n") - # This assertion passes whether showdoc dialog completed or not. assert_match(/start\ IRB\nirb\(main\):001> {}\.__id__\n }\.__id__(?:Press )?/, screen) end @@ -233,7 +276,9 @@ class IRB::RenderingTest < Yamatanooroti::TestCase puts 'start IRB' LINES start_terminal(4, 19, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') - write("IR\C-i") + write("IR") + write("\C-i") + sleep 0.2 close # This is because on macOS we display different shortcut for displaying the full doc @@ -269,7 +314,9 @@ class IRB::RenderingTest < Yamatanooroti::TestCase puts 'start IRB' LINES start_terminal(4, 12, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') - write("IR\C-i") + write("IR") + write("\C-i") + sleep 0.2 close assert_screen(<<~EOC) start IRB @@ -319,7 +366,7 @@ class IRB::RenderingTest < Yamatanooroti::TestCase puts 'start IRB' LINES start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') - write("show_cmds\n") + write("help\n") write("G") # move to the end of the screen write("\C-c") # quit pager write("'foo' + 'bar'\n") # eval something to make sure IRB resumes diff --git a/test/reline/helper.rb b/test/reline/helper.rb index fb2262e7f5..26fe834482 100644 --- a/test/reline/helper.rb +++ b/test/reline/helper.rb @@ -29,12 +29,19 @@ module Reline else encoding = Encoding::UTF_8 end + @original_get_screen_size = IOGate.method(:get_screen_size) + IOGate.singleton_class.remove_method(:get_screen_size) + def IOGate.get_screen_size + [24, 80] + end Reline::GeneralIO.reset(encoding: encoding) unless ansi core.config.instance_variable_set(:@test_mode, true) core.config.reset end def test_reset + IOGate.singleton_class.remove_method(:get_screen_size) + IOGate.define_singleton_method(:get_screen_size, @original_get_screen_size) remove_const('IOGate') const_set('IOGate', @original_iogate) Reline::GeneralIO.reset @@ -71,14 +78,6 @@ module Reline end end -def start_pasting - Reline::GeneralIO.start_pasting -end - -def finish_pasting - Reline::GeneralIO.finish_pasting -end - class Reline::TestCase < Test::Unit::TestCase private def convert_str(input, options = {}, normalized = nil) return nil if input.nil? @@ -129,9 +128,14 @@ class Reline::TestCase < Test::Unit::TestCase end end - def assert_line(expected) - expected = convert_str(expected) - assert_equal(expected, @line_editor.line) + def assert_line_around_cursor(before, after) + before = convert_str(before) + after = convert_str(after) + line = @line_editor.current_line + byte_pointer = @line_editor.instance_variable_get(:@byte_pointer) + actual_before = line.byteslice(0, byte_pointer) + actual_after = line.byteslice(byte_pointer..) + assert_equal([before, after], [actual_before, actual_after]) end def assert_byte_pointer_size(expected) @@ -146,14 +150,6 @@ class Reline::TestCase < Test::Unit::TestCase EOM end - def assert_cursor(expected) - assert_equal(expected, @line_editor.instance_variable_get(:@cursor)) - end - - def assert_cursor_max(expected) - assert_equal(expected, @line_editor.instance_variable_get(:@cursor_max)) - end - def assert_line_index(expected) assert_equal(expected, @line_editor.instance_variable_get(:@line_index)) end diff --git a/test/reline/test_ansi_with_terminfo.rb b/test/reline/test_ansi_with_terminfo.rb index 1387460659..e1c56b9ee1 100644 --- a/test/reline/test_ansi_with_terminfo.rb +++ b/test/reline/test_ansi_with_terminfo.rb @@ -109,4 +109,4 @@ class Reline::ANSI::TestWithTerminfo < Reline::TestCase assert_key_binding("\e ", :em_set_mark, [:emacs]) assert_key_binding("\C-x\C-x", :em_exchange_mark, [:emacs]) end -end if Reline::Terminfo.enabled? +end if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? diff --git a/test/reline/test_config.rb b/test/reline/test_config.rb index 9ead047ce4..6068292847 100644 --- a/test/reline/test_config.rb +++ b/test/reline/test_config.rb @@ -216,6 +216,38 @@ class Reline::Config::Test < Reline::TestCase end end + def test_nested_if_else + @config.read_lines(<<~LINES.lines) + $if Ruby + "\x1": "O" + $if NotRuby + "\x2": "X" + $else + "\x3": "O" + $if Ruby + "\x4": "O" + $else + "\x5": "X" + $endif + "\x6": "O" + $endif + "\x7": "O" + $else + "\x8": "X" + $if NotRuby + "\x9": "X" + $else + "\xA": "X" + $endif + "\xB": "X" + $endif + "\xC": "O" + LINES + keys = [0x1, 0x3, 0x4, 0x6, 0x7, 0xC] + key_bindings = keys.to_h { |k| [[k.ord], ['O'.ord]] } + assert_equal(key_bindings, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) + end + def test_unclosed_if e = assert_raise(Reline::Config::InvalidInputrc) do @config.read_lines(<<~LINES.lines, "INPUTRC") @@ -243,6 +275,78 @@ class Reline::Config::Test < Reline::TestCase assert_equal "INPUTRC:1: unmatched endif", e.message end + def test_if_with_mode + @config.read_lines(<<~LINES.lines) + $if mode=emacs + "\C-e": history-search-backward # comment + $else + "\C-f": history-search-forward + $endif + LINES + + assert_equal({[5] => :history_search_backward}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + end + + def test_else + @config.read_lines(<<~LINES.lines) + $if mode=vi + "\C-e": history-search-backward # comment + $else + "\C-f": history-search-forward + $endif + LINES + + assert_equal({[6] => :history_search_forward}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + end + + def test_if_with_invalid_mode + @config.read_lines(<<~LINES.lines) + $if mode=vim + "\C-e": history-search-backward + $else + "\C-f": history-search-forward # comment + $endif + LINES + + assert_equal({[6] => :history_search_forward}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + end + + def test_mode_label_differs_from_keymap_label + @config.read_lines(<<~LINES.lines) + # Sets mode_label and keymap_label to vi + set editing-mode vi + # Change keymap_label to emacs. mode_label is still vi. + set keymap emacs + # condition=true because current mode_label is vi + $if mode=vi + # sets keybinding to current keymap_label=emacs + "\C-e": history-search-backward + $endif + LINES + assert_equal({[5] => :history_search_backward}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + end + + def test_if_without_else_condition + @config.read_lines(<<~LINES.lines) + set editing-mode vi + $if mode=vi + "\C-e": history-search-backward + $endif + LINES + + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:emacs]) + assert_equal({[5] => :history_search_backward}, @config.instance_variable_get(:@additional_key_bindings)[:vi_insert]) + assert_equal({}, @config.instance_variable_get(:@additional_key_bindings)[:vi_command]) + end + def test_default_key_bindings @config.add_default_key_binding('abcd'.bytes, 'EFGH'.bytes) @config.read_lines(<<~'LINES'.lines) @@ -313,6 +417,18 @@ class Reline::Config::Test < Reline::TestCase assert_equal expected, @config.key_bindings end + def test_key_bindings_with_reset + # @config.reset is called after each readline. + # inputrc file is read once, so key binding shouldn't be cleared by @config.reset + @config.add_default_key_binding('default'.bytes, 'DEFAULT'.bytes) + @config.read_lines(<<~'LINES'.lines) + "additional": "ADDITIONAL" + LINES + @config.reset + expected = { 'default'.bytes => 'DEFAULT'.bytes, 'additional'.bytes => 'ADDITIONAL'.bytes } + assert_equal expected, @config.key_bindings + end + def test_history_size @config.read_lines(<<~LINES.lines) set history-size 5000 diff --git a/test/reline/test_key_actor_emacs.rb b/test/reline/test_key_actor_emacs.rb index 4575e51eb2..013ca2f7b3 100644 --- a/test/reline/test_key_actor_emacs.rb +++ b/test/reline/test_key_actor_emacs.rb @@ -19,1259 +19,699 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase def test_ed_insert_one input_keys('a') - assert_line('a') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(1) + assert_line_around_cursor('a', '') end def test_ed_insert_two input_keys('ab') - assert_line('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') end def test_ed_insert_mbchar_one input_keys('か') - assert_line('か') - assert_byte_pointer_size('か') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('か', '') end def test_ed_insert_mbchar_two input_keys('かき') - assert_line('かき') - assert_byte_pointer_size('かき') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('かき', '') end def test_ed_insert_for_mbchar_by_plural_code_points input_keys("か\u3099") - assert_line("か\u3099") - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor("か\u3099", '') end def test_ed_insert_for_plural_mbchar_by_plural_code_points input_keys("か\u3099き\u3099") - assert_line("か\u3099き\u3099") - assert_byte_pointer_size("か\u3099き\u3099") - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor("か\u3099き\u3099", '') end def test_move_next_and_prev input_keys('abd') - assert_byte_pointer_size('abd') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abd', '') input_keys("\C-b", false) - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(3) + assert_line_around_cursor('ab', 'd') input_keys("\C-b", false) - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(3) + assert_line_around_cursor('a', 'bd') input_keys("\C-f", false) - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(3) + assert_line_around_cursor('ab', 'd') input_keys('c') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(4) - assert_line('abcd') + assert_line_around_cursor('abc', 'd') end def test_move_next_and_prev_for_mbchar input_keys('かきけ') - assert_byte_pointer_size('かきけ') - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor('かきけ', '') input_keys("\C-b", false) - assert_byte_pointer_size('かき') - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor('かき', 'け') input_keys("\C-b", false) - assert_byte_pointer_size('か') - assert_cursor(2) - assert_cursor_max(6) + assert_line_around_cursor('か', 'きけ') input_keys("\C-f", false) - assert_byte_pointer_size('かき') - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor('かき', 'け') input_keys('く') - assert_byte_pointer_size('かきく') - assert_cursor(6) - assert_cursor_max(8) - assert_line('かきくけ') + assert_line_around_cursor('かきく', 'け') end def test_move_next_and_prev_for_mbchar_by_plural_code_points input_keys("か\u3099き\u3099け\u3099") - assert_byte_pointer_size("か\u3099き\u3099け\u3099") - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099き\u3099け\u3099", '') input_keys("\C-b", false) - assert_byte_pointer_size("か\u3099き\u3099") - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099き\u3099", "け\u3099") input_keys("\C-b", false) - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099", "き\u3099け\u3099") input_keys("\C-f", false) - assert_byte_pointer_size("か\u3099き\u3099") - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099き\u3099", "け\u3099") input_keys("く\u3099") - assert_byte_pointer_size("か\u3099き\u3099く\u3099") - assert_cursor(6) - assert_cursor_max(8) - assert_line("か\u3099き\u3099く\u3099け\u3099") + assert_line_around_cursor("か\u3099き\u3099く\u3099", "け\u3099") end def test_move_to_beg_end input_keys('bcd') - assert_byte_pointer_size('bcd') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('bcd', '') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) + assert_line_around_cursor('', 'bcd') input_keys('a') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(4) + assert_line_around_cursor('a', 'bcd') input_keys("\C-e", false) - assert_byte_pointer_size('abcd') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('abcd', '') input_keys('e') - assert_byte_pointer_size('abcde') - assert_cursor(5) - assert_cursor_max(5) - assert_line('abcde') + assert_line_around_cursor('abcde', '') end def test_ed_newline_with_cr input_keys('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') refute(@line_editor.finished?) input_keys("\C-m", false) - assert_line('ab') + assert_line_around_cursor('ab', '') assert(@line_editor.finished?) end def test_ed_newline_with_lf input_keys('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') refute(@line_editor.finished?) input_keys("\C-j", false) - assert_line('ab') + assert_line_around_cursor('ab', '') assert(@line_editor.finished?) end def test_em_delete_prev_char input_keys('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') input_keys("\C-h", false) - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(1) - assert_line('a') + assert_line_around_cursor('a', '') end def test_em_delete_prev_char_for_mbchar input_keys('かき') - assert_byte_pointer_size('かき') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('かき', '') input_keys("\C-h", false) - assert_byte_pointer_size('か') - assert_cursor(2) - assert_cursor_max(2) - assert_line('か') + assert_line_around_cursor('か', '') end def test_em_delete_prev_char_for_mbchar_by_plural_code_points input_keys("か\u3099き\u3099") - assert_byte_pointer_size("か\u3099き\u3099") - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor("か\u3099き\u3099", '') input_keys("\C-h", false) - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(2) - assert_line("か\u3099") + assert_line_around_cursor("か\u3099", '') end def test_ed_quoted_insert input_keys("ab\C-v\C-acd") - assert_line("ab\C-acd") - assert_byte_pointer_size("ab\C-acd") - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor("ab\C-acd", '') input_keys("\C-q\C-b") - assert_line("ab\C-acd\C-b") - assert_byte_pointer_size("ab\C-acd\C-b") - assert_cursor(8) - assert_cursor_max(8) + assert_line_around_cursor("ab\C-acd\C-b", '') end def test_ed_kill_line input_keys("\C-k", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('abc') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abc', '') input_keys("\C-k", false) - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(3) - assert_line('abc') + assert_line_around_cursor('abc', '') input_keys("\C-b\C-k", false) - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) - assert_line('ab') + assert_line_around_cursor('ab', '') end def test_em_kill_line @line_editor.input_key(Reline::Key.new(:em_kill_line, :em_kill_line, false)) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('abc') @line_editor.input_key(Reline::Key.new(:em_kill_line, :em_kill_line, false)) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('abc') input_keys("\C-b", false) @line_editor.input_key(Reline::Key.new(:em_kill_line, :em_kill_line, false)) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('abc') input_keys("\C-a", false) @line_editor.input_key(Reline::Key.new(:em_kill_line, :em_kill_line, false)) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_ed_move_to_beg input_keys('abd') - assert_byte_pointer_size('abd') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abd', '') input_keys("\C-b", false) - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(3) + assert_line_around_cursor('ab', 'd') input_keys('c') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(4) + assert_line_around_cursor('abc', 'd') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(4) + assert_line_around_cursor('', 'abcd') input_keys('012') - assert_byte_pointer_size('012') - assert_cursor(3) - assert_cursor_max(7) - assert_line('012abcd') + assert_line_around_cursor('012', 'abcd') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(7) + assert_line_around_cursor('', '012abcd') input_keys('ABC') - assert_byte_pointer_size('ABC') - assert_cursor(3) - assert_cursor_max(10) - assert_line('ABC012abcd') + assert_line_around_cursor('ABC', '012abcd') input_keys("\C-f" * 10 + "\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(10) + assert_line_around_cursor('', 'ABC012abcd') input_keys('a') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(11) - assert_line('aABC012abcd') + assert_line_around_cursor('a', 'ABC012abcd') end def test_ed_move_to_beg_with_blank input_keys(' abc') - assert_byte_pointer_size(' abc') - assert_cursor(5) - assert_cursor_max(5) + assert_line_around_cursor(' abc', '') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) + assert_line_around_cursor('', ' abc') end def test_ed_move_to_end input_keys('abd') - assert_byte_pointer_size('abd') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abd', '') input_keys("\C-b", false) - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(3) + assert_line_around_cursor('ab', 'd') input_keys('c') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(4) + assert_line_around_cursor('abc', 'd') input_keys("\C-e", false) - assert_byte_pointer_size('abcd') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('abcd', '') input_keys('012') - assert_byte_pointer_size('abcd012') - assert_cursor(7) - assert_cursor_max(7) - assert_line('abcd012') + assert_line_around_cursor('abcd012', '') input_keys("\C-e", false) - assert_byte_pointer_size('abcd012') - assert_cursor(7) - assert_cursor_max(7) + assert_line_around_cursor('abcd012', '') input_keys('ABC') - assert_byte_pointer_size('abcd012ABC') - assert_cursor(10) - assert_cursor_max(10) - assert_line('abcd012ABC') + assert_line_around_cursor('abcd012ABC', '') input_keys("\C-b" * 10 + "\C-e", false) - assert_byte_pointer_size('abcd012ABC') - assert_cursor(10) - assert_cursor_max(10) + assert_line_around_cursor('abcd012ABC', '') input_keys('a') - assert_byte_pointer_size('abcd012ABCa') - assert_cursor(11) - assert_cursor_max(11) - assert_line('abcd012ABCa') + assert_line_around_cursor('abcd012ABCa', '') end def test_em_delete input_keys('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) + assert_line_around_cursor('', 'ab') input_keys("\C-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(1) - assert_line('b') + assert_line_around_cursor('', 'b') end def test_em_delete_for_mbchar input_keys('かき') - assert_byte_pointer_size('かき') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('かき', '') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(4) + assert_line_around_cursor('', 'かき') input_keys("\C-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) - assert_line('き') + assert_line_around_cursor('', 'き') end def test_em_delete_for_mbchar_by_plural_code_points input_keys("か\u3099き\u3099") - assert_byte_pointer_size("か\u3099き\u3099") - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor("か\u3099き\u3099", '') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(4) + assert_line_around_cursor('', "か\u3099き\u3099") input_keys("\C-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) - assert_line("き\u3099") + assert_line_around_cursor('', "き\u3099") end def test_em_delete_ends_editing input_keys("\C-d") # quit from inputing - assert_line(nil) + assert_nil(@line_editor.line) assert(@line_editor.finished?) end def test_ed_clear_screen - refute(@line_editor.instance_variable_get(:@cleared)) + @line_editor.instance_variable_get(:@rendered_screen).lines = [[]] input_keys("\C-l", false) - assert(@line_editor.instance_variable_get(:@cleared)) + assert_empty(@line_editor.instance_variable_get(:@rendered_screen).lines) end def test_ed_clear_screen_with_inputed input_keys('abc') input_keys("\C-b", false) - refute(@line_editor.instance_variable_get(:@cleared)) - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(3) + @line_editor.instance_variable_get(:@rendered_screen).lines = [[]] + assert_line_around_cursor('ab', 'c') input_keys("\C-l", false) - assert(@line_editor.instance_variable_get(:@cleared)) - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(3) - assert_line('abc') + assert_empty(@line_editor.instance_variable_get(:@rendered_screen).lines) + assert_line_around_cursor('ab', 'c') end def test_key_delete input_keys('abc') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abc', '') @line_editor.input_key(Reline::Key.new(:key_delete, :key_delete, false)) - assert_cursor(3) - assert_cursor_max(3) - assert_line('abc') + assert_line_around_cursor('abc', '') end def test_key_delete_does_not_end_editing @line_editor.input_key(Reline::Key.new(:key_delete, :key_delete, false)) - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') refute(@line_editor.finished?) end def test_key_delete_preserves_cursor input_keys('abc') input_keys("\C-b", false) - assert_cursor(2) - assert_cursor_max(3) + assert_line_around_cursor('ab', 'c') @line_editor.input_key(Reline::Key.new(:key_delete, :key_delete, false)) - assert_cursor(2) - assert_cursor_max(2) - assert_line('ab') + assert_line_around_cursor('ab', '') end def test_em_next_word - assert_byte_pointer_size('') - assert_cursor(0) + assert_line_around_cursor('', '') input_keys('abc def{bbb}ccc') input_keys("\C-a\M-F", false) - assert_byte_pointer_size('abc') - assert_cursor(3) + assert_line_around_cursor('abc', ' def{bbb}ccc') input_keys("\M-F", false) - assert_byte_pointer_size('abc def') - assert_cursor(7) + assert_line_around_cursor('abc def', '{bbb}ccc') input_keys("\M-F", false) - assert_byte_pointer_size('abc def{bbb') - assert_cursor(11) + assert_line_around_cursor('abc def{bbb', '}ccc') input_keys("\M-F", false) - assert_byte_pointer_size('abc def{bbb}ccc') - assert_cursor(15) + assert_line_around_cursor('abc def{bbb}ccc', '') input_keys("\M-F", false) - assert_byte_pointer_size('abc def{bbb}ccc') - assert_cursor(15) + assert_line_around_cursor('abc def{bbb}ccc', '') end def test_em_next_word_for_mbchar - assert_cursor(0) + assert_line_around_cursor('', '') input_keys('あいう かきく{さしす}たちつ') input_keys("\C-a\M-F", false) - assert_byte_pointer_size('あいう') - assert_cursor(6) + assert_line_around_cursor('あいう', ' かきく{さしす}たちつ') input_keys("\M-F", false) - assert_byte_pointer_size('あいう かきく') - assert_cursor(13) + assert_line_around_cursor('あいう かきく', '{さしす}たちつ') input_keys("\M-F", false) - assert_byte_pointer_size('あいう かきく{さしす') - assert_cursor(20) + assert_line_around_cursor('あいう かきく{さしす', '}たちつ') input_keys("\M-F", false) - assert_byte_pointer_size('あいう かきく{さしす}たちつ') - assert_cursor(27) + assert_line_around_cursor('あいう かきく{さしす}たちつ', '') input_keys("\M-F", false) - assert_byte_pointer_size('あいう かきく{さしす}たちつ') - assert_cursor(27) + assert_line_around_cursor('あいう かきく{さしす}たちつ', '') end def test_em_next_word_for_mbchar_by_plural_code_points - assert_cursor(0) + assert_line_around_cursor("", "") input_keys("あいう か\u3099き\u3099く\u3099{さしす}たちつ") input_keys("\C-a\M-F", false) - assert_byte_pointer_size("あいう") - assert_cursor(6) + assert_line_around_cursor("あいう", " か\u3099き\u3099く\u3099{さしす}たちつ") input_keys("\M-F", false) - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099") - assert_cursor(13) + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099", "{さしす}たちつ") input_keys("\M-F", false) - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{さしす") - assert_cursor(20) + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{さしす", "}たちつ") input_keys("\M-F", false) - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{さしす}たちつ") - assert_cursor(27) + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{さしす}たちつ", "") input_keys("\M-F", false) - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{さしす}たちつ") - assert_cursor(27) + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{さしす}たちつ", "") end def test_em_prev_word input_keys('abc def{bbb}ccc') - assert_byte_pointer_size('abc def{bbb}ccc') - assert_cursor(15) + assert_line_around_cursor('abc def{bbb}ccc', '') input_keys("\M-B", false) - assert_byte_pointer_size('abc def{bbb}') - assert_cursor(12) + assert_line_around_cursor('abc def{bbb}', 'ccc') input_keys("\M-B", false) - assert_byte_pointer_size('abc def{') - assert_cursor(8) + assert_line_around_cursor('abc def{', 'bbb}ccc') input_keys("\M-B", false) - assert_byte_pointer_size('abc ') - assert_cursor(4) + assert_line_around_cursor('abc ', 'def{bbb}ccc') input_keys("\M-B", false) - assert_byte_pointer_size('') - assert_cursor(0) + assert_line_around_cursor('', 'abc def{bbb}ccc') input_keys("\M-B", false) - assert_byte_pointer_size('') - assert_cursor(0) + assert_line_around_cursor('', 'abc def{bbb}ccc') end def test_em_prev_word_for_mbchar input_keys('あいう かきく{さしす}たちつ') - assert_byte_pointer_size('あいう かきく{さしす}たちつ') - assert_cursor(27) + assert_line_around_cursor('あいう かきく{さしす}たちつ', '') input_keys("\M-B", false) - assert_byte_pointer_size('あいう かきく{さしす}') - assert_cursor(21) + assert_line_around_cursor('あいう かきく{さしす}', 'たちつ') input_keys("\M-B", false) - assert_byte_pointer_size('あいう かきく{') - assert_cursor(14) + assert_line_around_cursor('あいう かきく{', 'さしす}たちつ') input_keys("\M-B", false) - assert_byte_pointer_size('あいう ') - assert_cursor(7) + assert_line_around_cursor('あいう ', 'かきく{さしす}たちつ') input_keys("\M-B", false) - assert_byte_pointer_size('') - assert_cursor(0) + assert_line_around_cursor('', 'あいう かきく{さしす}たちつ') input_keys("\M-B", false) - assert_byte_pointer_size('') - assert_cursor(0) + assert_line_around_cursor('', 'あいう かきく{さしす}たちつ') end def test_em_prev_word_for_mbchar_by_plural_code_points input_keys("あいう か\u3099き\u3099く\u3099{さしす}たちつ") - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{さしす}たちつ") - assert_cursor(27) + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{さしす}たちつ", "") input_keys("\M-B", false) - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{さしす}") - assert_cursor(21) + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{さしす}", "たちつ") input_keys("\M-B", false) - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{") - assert_cursor(14) + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{", "さしす}たちつ") input_keys("\M-B", false) - assert_byte_pointer_size('あいう ') - assert_cursor(7) + assert_line_around_cursor("あいう ", "か\u3099き\u3099く\u3099{さしす}たちつ") input_keys("\M-B", false) - assert_byte_pointer_size('') - assert_cursor(0) + assert_line_around_cursor("", "あいう か\u3099き\u3099く\u3099{さしす}たちつ") input_keys("\M-B", false) - assert_byte_pointer_size('') - assert_cursor(0) + assert_line_around_cursor("", "あいう か\u3099き\u3099く\u3099{さしす}たちつ") end def test_em_delete_next_word input_keys('abc def{bbb}ccc') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(15) + assert_line_around_cursor('', 'abc def{bbb}ccc') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(12) - assert_line(' def{bbb}ccc') + assert_line_around_cursor('', ' def{bbb}ccc') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(8) - assert_line('{bbb}ccc') + assert_line_around_cursor('', '{bbb}ccc') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(4) - assert_line('}ccc') + assert_line_around_cursor('', '}ccc') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_em_delete_next_word_for_mbchar input_keys('あいう かきく{さしす}たちつ') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(27) + assert_line_around_cursor('', 'あいう かきく{さしす}たちつ') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(21) - assert_line(' かきく{さしす}たちつ') + assert_line_around_cursor('', ' かきく{さしす}たちつ') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(14) - assert_line('{さしす}たちつ') + assert_line_around_cursor('', '{さしす}たちつ') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(7) - assert_line('}たちつ') + assert_line_around_cursor('', '}たちつ') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_em_delete_next_word_for_mbchar_by_plural_code_points input_keys("あいう か\u3099き\u3099く\u3099{さしす}たちつ") input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(27) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(27) + assert_line_around_cursor('', "あいう か\u3099き\u3099く\u3099{さしす}たちつ") input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(21) - assert_line(" か\u3099き\u3099く\u3099{さしす}たちつ") + assert_line_around_cursor('', " か\u3099き\u3099く\u3099{さしす}たちつ") input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(14) - assert_line('{さしす}たちつ') + assert_line_around_cursor('', '{さしす}たちつ') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(7) - assert_line('}たちつ') + assert_line_around_cursor('', '}たちつ') input_keys("\M-d", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_ed_delete_prev_word input_keys('abc def{bbb}ccc') - assert_byte_pointer_size('abc def{bbb}ccc') - assert_cursor(15) - assert_cursor_max(15) + assert_line_around_cursor('abc def{bbb}ccc', '') input_keys("\M-\C-H", false) - assert_byte_pointer_size('abc def{bbb}') - assert_cursor(12) - assert_cursor_max(12) - assert_line('abc def{bbb}') + assert_line_around_cursor('abc def{bbb}', '') input_keys("\M-\C-H", false) - assert_byte_pointer_size('abc def{') - assert_cursor(8) - assert_cursor_max(8) - assert_line('abc def{') + assert_line_around_cursor('abc def{', '') input_keys("\M-\C-H", false) - assert_byte_pointer_size('abc ') - assert_cursor(4) - assert_cursor_max(4) - assert_line('abc ') + assert_line_around_cursor('abc ', '') input_keys("\M-\C-H", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_ed_delete_prev_word_for_mbchar input_keys('あいう かきく{さしす}たちつ') - assert_byte_pointer_size('あいう かきく{さしす}たちつ') - assert_cursor(27) - assert_cursor_max(27) + assert_line_around_cursor('あいう かきく{さしす}たちつ', '') input_keys("\M-\C-H", false) - assert_byte_pointer_size('あいう かきく{さしす}') - assert_cursor(21) - assert_cursor_max(21) - assert_line('あいう かきく{さしす}') + assert_line_around_cursor('あいう かきく{さしす}', '') input_keys("\M-\C-H", false) - assert_byte_pointer_size('あいう かきく{') - assert_cursor(14) - assert_cursor_max(14) - assert_line('あいう かきく{') + assert_line_around_cursor('あいう かきく{', '') input_keys("\M-\C-H", false) - assert_byte_pointer_size('あいう ') - assert_cursor(7) - assert_cursor_max(7) - assert_line('あいう ') + assert_line_around_cursor('あいう ', '') input_keys("\M-\C-H", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_ed_delete_prev_word_for_mbchar_by_plural_code_points input_keys("あいう か\u3099き\u3099く\u3099{さしす}たちつ") - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{さしす}たちつ") - assert_cursor(27) - assert_cursor_max(27) + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{さしす}たちつ", '') input_keys("\M-\C-H", false) - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{さしす}") - assert_cursor(21) - assert_cursor_max(21) - assert_line("あいう か\u3099き\u3099く\u3099{さしす}") + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{さしす}", '') input_keys("\M-\C-H", false) - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{") - assert_cursor(14) - assert_cursor_max(14) - assert_line("あいう か\u3099き\u3099く\u3099{") + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{", '') input_keys("\M-\C-H", false) - assert_byte_pointer_size("あいう ") - assert_cursor(7) - assert_cursor_max(7) - assert_line('あいう ') + assert_line_around_cursor('あいう ', '') input_keys("\M-\C-H", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_ed_transpose_chars input_keys('abc') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) + assert_line_around_cursor('', 'abc') input_keys("\C-t", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) - assert_line('abc') + assert_line_around_cursor('', 'abc') input_keys("\C-f\C-t", false) - assert_byte_pointer_size('ba') - assert_cursor(2) - assert_cursor_max(3) - assert_line('bac') + assert_line_around_cursor('ba', 'c') input_keys("\C-t", false) - assert_byte_pointer_size('bca') - assert_cursor(3) - assert_cursor_max(3) - assert_line('bca') + assert_line_around_cursor('bca', '') input_keys("\C-t", false) - assert_byte_pointer_size('bac') - assert_cursor(3) - assert_cursor_max(3) - assert_line('bac') + assert_line_around_cursor('bac', '') end def test_ed_transpose_chars_for_mbchar input_keys('あかさ') input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'あかさ') input_keys("\C-t", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) - assert_line('あかさ') + assert_line_around_cursor('', 'あかさ') input_keys("\C-f\C-t", false) - assert_byte_pointer_size('かあ') - assert_cursor(4) - assert_cursor_max(6) - assert_line('かあさ') + assert_line_around_cursor('かあ', 'さ') input_keys("\C-t", false) - assert_byte_pointer_size('かさあ') - assert_cursor(6) - assert_cursor_max(6) - assert_line('かさあ') + assert_line_around_cursor('かさあ', '') input_keys("\C-t", false) - assert_byte_pointer_size('かあさ') - assert_cursor(6) - assert_cursor_max(6) - assert_line('かあさ') + assert_line_around_cursor('かあさ', '') end def test_ed_transpose_chars_for_mbchar_by_plural_code_points input_keys("あか\u3099さ") input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', "あか\u3099さ") input_keys("\C-t", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) - assert_line("あか\u3099さ") + assert_line_around_cursor('', "あか\u3099さ") input_keys("\C-f\C-t", false) - assert_byte_pointer_size("か\u3099あ") - assert_cursor(4) - assert_cursor_max(6) - assert_line("か\u3099あさ") + assert_line_around_cursor("か\u3099あ", 'さ') input_keys("\C-t", false) - assert_byte_pointer_size("か\u3099さあ") - assert_cursor(6) - assert_cursor_max(6) - assert_line("か\u3099さあ") + assert_line_around_cursor("か\u3099さあ", '') input_keys("\C-t", false) - assert_byte_pointer_size("か\u3099あさ") - assert_cursor(6) - assert_cursor_max(6) - assert_line("か\u3099あさ") + assert_line_around_cursor("か\u3099あさ", '') end def test_ed_transpose_words input_keys('abc def') - assert_line('abc def') - assert_byte_pointer_size('abc def') - assert_cursor(7) - assert_cursor_max(7) + assert_line_around_cursor('abc def', '') input_keys("\M-t", false) - assert_line('def abc') - assert_byte_pointer_size('def abc') - assert_cursor(7) - assert_cursor_max(7) + assert_line_around_cursor('def abc', '') input_keys("\C-a\C-k", false) input_keys(' abc def ') input_keys("\C-b" * 4, false) - assert_line(' abc def ') - assert_byte_pointer_size(' abc de') - assert_cursor(8) - assert_cursor_max(12) + assert_line_around_cursor(' abc de', 'f ') input_keys("\M-t", false) - assert_line(' def abc ') - assert_byte_pointer_size(' def abc') - assert_cursor(9) - assert_cursor_max(12) + assert_line_around_cursor(' def abc', ' ') input_keys("\C-a\C-k", false) input_keys(' abc def ') input_keys("\C-b" * 6, false) - assert_line(' abc def ') - assert_byte_pointer_size(' abc ') - assert_cursor(6) - assert_cursor_max(12) + assert_line_around_cursor(' abc ', 'def ') input_keys("\M-t", false) - assert_line(' def abc ') - assert_byte_pointer_size(' def abc') - assert_cursor(9) - assert_cursor_max(12) + assert_line_around_cursor(' def abc', ' ') input_keys("\M-t", false) - assert_line(' abc def') - assert_byte_pointer_size(' abc def') - assert_cursor(12) - assert_cursor_max(12) + assert_line_around_cursor(' abc def', '') end def test_ed_transpose_words_for_mbchar input_keys('あいう かきく') - assert_line('あいう かきく') - assert_byte_pointer_size('あいう かきく') - assert_cursor(13) - assert_cursor_max(13) + assert_line_around_cursor('あいう かきく', '') input_keys("\M-t", false) - assert_line('かきく あいう') - assert_byte_pointer_size('かきく あいう') - assert_cursor(13) - assert_cursor_max(13) + assert_line_around_cursor('かきく あいう', '') input_keys("\C-a\C-k", false) input_keys(' あいう かきく ') input_keys("\C-b" * 4, false) - assert_line(' あいう かきく ') - assert_byte_pointer_size(' あいう かき') - assert_cursor(13) - assert_cursor_max(18) + assert_line_around_cursor(' あいう かき', 'く ') input_keys("\M-t", false) - assert_line(' かきく あいう ') - assert_byte_pointer_size(' かきく あいう') - assert_cursor(15) - assert_cursor_max(18) + assert_line_around_cursor(' かきく あいう', ' ') input_keys("\C-a\C-k", false) input_keys(' あいう かきく ') input_keys("\C-b" * 6, false) - assert_line(' あいう かきく ') - assert_byte_pointer_size(' あいう ') - assert_cursor(9) - assert_cursor_max(18) + assert_line_around_cursor(' あいう ', 'かきく ') input_keys("\M-t", false) - assert_line(' かきく あいう ') - assert_byte_pointer_size(' かきく あいう') - assert_cursor(15) - assert_cursor_max(18) + assert_line_around_cursor(' かきく あいう', ' ') input_keys("\M-t", false) - assert_line(' あいう かきく') - assert_byte_pointer_size(' あいう かきく') - assert_cursor(18) - assert_cursor_max(18) + assert_line_around_cursor(' あいう かきく', '') end def test_ed_transpose_words_with_one_word input_keys('abc ') - assert_line('abc ') - assert_byte_pointer_size('abc ') - assert_cursor(5) - assert_cursor_max(5) + assert_line_around_cursor('abc ', '') input_keys("\M-t", false) - assert_line('abc ') - assert_byte_pointer_size('abc ') - assert_cursor(5) - assert_cursor_max(5) + assert_line_around_cursor('abc ', '') input_keys("\C-b", false) - assert_line('abc ') - assert_byte_pointer_size('abc ') - assert_cursor(4) - assert_cursor_max(5) + assert_line_around_cursor('abc ', ' ') input_keys("\M-t", false) - assert_line('abc ') - assert_byte_pointer_size('abc ') - assert_cursor(4) - assert_cursor_max(5) + assert_line_around_cursor('abc ', ' ') input_keys("\C-b" * 2, false) - assert_line('abc ') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(5) + assert_line_around_cursor('ab', 'c ') input_keys("\M-t", false) - assert_line('abc ') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(5) + assert_line_around_cursor('ab', 'c ') input_keys("\M-t", false) - assert_line('abc ') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(5) + assert_line_around_cursor('ab', 'c ') end def test_ed_transpose_words_with_one_word_for_mbchar input_keys('あいう ') - assert_line('あいう ') - assert_byte_pointer_size('あいう ') - assert_cursor(8) - assert_cursor_max(8) + assert_line_around_cursor('あいう ', '') input_keys("\M-t", false) - assert_line('あいう ') - assert_byte_pointer_size('あいう ') - assert_cursor(8) - assert_cursor_max(8) + assert_line_around_cursor('あいう ', '') input_keys("\C-b", false) - assert_line('あいう ') - assert_byte_pointer_size('あいう ') - assert_cursor(7) - assert_cursor_max(8) + assert_line_around_cursor('あいう ', ' ') input_keys("\M-t", false) - assert_line('あいう ') - assert_byte_pointer_size('あいう ') - assert_cursor(7) - assert_cursor_max(8) + assert_line_around_cursor('あいう ', ' ') input_keys("\C-b" * 2, false) - assert_line('あいう ') - assert_byte_pointer_size('あい') - assert_cursor(4) - assert_cursor_max(8) + assert_line_around_cursor('あい', 'う ') input_keys("\M-t", false) - assert_line('あいう ') - assert_byte_pointer_size('あい') - assert_cursor(4) - assert_cursor_max(8) + assert_line_around_cursor('あい', 'う ') input_keys("\M-t", false) - assert_line('あいう ') - assert_byte_pointer_size('あい') - assert_cursor(4) - assert_cursor_max(8) + assert_line_around_cursor('あい', 'う ') end def test_ed_digit input_keys('0123') - assert_byte_pointer_size('0123') - assert_cursor(4) - assert_cursor_max(4) - assert_line('0123') + assert_line_around_cursor('0123', '') end def test_ed_next_and_prev_char input_keys('abc') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abc', '') input_keys("\C-b", false) - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(3) + assert_line_around_cursor('ab', 'c') input_keys("\C-b", false) - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(3) + assert_line_around_cursor('a', 'bc') input_keys("\C-b", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) + assert_line_around_cursor('', 'abc') input_keys("\C-b", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) + assert_line_around_cursor('', 'abc') input_keys("\C-f", false) - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(3) + assert_line_around_cursor('a', 'bc') input_keys("\C-f", false) - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(3) + assert_line_around_cursor('ab', 'c') input_keys("\C-f", false) - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abc', '') input_keys("\C-f", false) - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abc', '') end def test_ed_next_and_prev_char_for_mbchar input_keys('あいう') - assert_byte_pointer_size('あいう') - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor('あいう', '') input_keys("\C-b", false) - assert_byte_pointer_size('あい') - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor('あい', 'う') input_keys("\C-b", false) - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(6) + assert_line_around_cursor('あ', 'いう') input_keys("\C-b", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'あいう') input_keys("\C-b", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'あいう') input_keys("\C-f", false) - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(6) + assert_line_around_cursor('あ', 'いう') input_keys("\C-f", false) - assert_byte_pointer_size('あい') - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor('あい', 'う') input_keys("\C-f", false) - assert_byte_pointer_size('あいう') - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor('あいう', '') input_keys("\C-f", false) - assert_byte_pointer_size('あいう') - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor('あいう', '') end def test_ed_next_and_prev_char_for_mbchar_by_plural_code_points input_keys("か\u3099き\u3099く\u3099") - assert_byte_pointer_size("か\u3099き\u3099く\u3099") - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099き\u3099く\u3099", '') input_keys("\C-b", false) - assert_byte_pointer_size("か\u3099き\u3099") - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099き\u3099", "く\u3099") input_keys("\C-b", false) - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099", "き\u3099く\u3099") input_keys("\C-b", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', "か\u3099き\u3099く\u3099") input_keys("\C-b", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', "か\u3099き\u3099く\u3099") input_keys("\C-f", false) - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099", "き\u3099く\u3099") input_keys("\C-f", false) - assert_byte_pointer_size("か\u3099き\u3099") - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099き\u3099", "く\u3099") input_keys("\C-f", false) - assert_byte_pointer_size("か\u3099き\u3099く\u3099") - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099き\u3099く\u3099", '') input_keys("\C-f", false) - assert_byte_pointer_size("か\u3099き\u3099く\u3099") - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor("か\u3099き\u3099く\u3099", '') end def test_em_capitol_case input_keys('abc def{bbb}ccc') input_keys("\C-a\M-c", false) - assert_byte_pointer_size('Abc') - assert_cursor(3) - assert_cursor_max(15) - assert_line('Abc def{bbb}ccc') + assert_line_around_cursor('Abc', ' def{bbb}ccc') input_keys("\M-c", false) - assert_byte_pointer_size('Abc Def') - assert_cursor(7) - assert_cursor_max(15) - assert_line('Abc Def{bbb}ccc') + assert_line_around_cursor('Abc Def', '{bbb}ccc') input_keys("\M-c", false) - assert_byte_pointer_size('Abc Def{Bbb') - assert_cursor(11) - assert_cursor_max(15) - assert_line('Abc Def{Bbb}ccc') + assert_line_around_cursor('Abc Def{Bbb', '}ccc') input_keys("\M-c", false) - assert_byte_pointer_size('Abc Def{Bbb}Ccc') - assert_cursor(15) - assert_cursor_max(15) - assert_line('Abc Def{Bbb}Ccc') + assert_line_around_cursor('Abc Def{Bbb}Ccc', '') end def test_em_capitol_case_with_complex_example input_keys('{}#* AaA!!!cCc ') input_keys("\C-a\M-c", false) - assert_byte_pointer_size('{}#* Aaa') - assert_cursor(11) - assert_cursor_max(20) - assert_line('{}#* Aaa!!!cCc ') + assert_line_around_cursor('{}#* Aaa', '!!!cCc ') input_keys("\M-c", false) - assert_byte_pointer_size('{}#* Aaa!!!Ccc') - assert_cursor(17) - assert_cursor_max(20) - assert_line('{}#* Aaa!!!Ccc ') + assert_line_around_cursor('{}#* Aaa!!!Ccc', ' ') input_keys("\M-c", false) - assert_byte_pointer_size('{}#* Aaa!!!Ccc ') - assert_cursor(20) - assert_cursor_max(20) - assert_line('{}#* Aaa!!!Ccc ') + assert_line_around_cursor('{}#* Aaa!!!Ccc ', '') end def test_em_lower_case input_keys('AbC def{bBb}CCC') input_keys("\C-a\M-l", false) - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(15) - assert_line('abc def{bBb}CCC') + assert_line_around_cursor('abc', ' def{bBb}CCC') input_keys("\M-l", false) - assert_byte_pointer_size('abc def') - assert_cursor(7) - assert_cursor_max(15) - assert_line('abc def{bBb}CCC') + assert_line_around_cursor('abc def', '{bBb}CCC') input_keys("\M-l", false) - assert_byte_pointer_size('abc def{bbb') - assert_cursor(11) - assert_cursor_max(15) - assert_line('abc def{bbb}CCC') + assert_line_around_cursor('abc def{bbb', '}CCC') input_keys("\M-l", false) - assert_byte_pointer_size('abc def{bbb}ccc') - assert_cursor(15) - assert_cursor_max(15) - assert_line('abc def{bbb}ccc') + assert_line_around_cursor('abc def{bbb}ccc', '') end def test_em_lower_case_with_complex_example input_keys('{}#* AaA!!!cCc ') input_keys("\C-a\M-l", false) - assert_byte_pointer_size('{}#* aaa') - assert_cursor(11) - assert_cursor_max(20) - assert_line('{}#* aaa!!!cCc ') + assert_line_around_cursor('{}#* aaa', '!!!cCc ') input_keys("\M-l", false) - assert_byte_pointer_size('{}#* aaa!!!ccc') - assert_cursor(17) - assert_cursor_max(20) - assert_line('{}#* aaa!!!ccc ') + assert_line_around_cursor('{}#* aaa!!!ccc', ' ') input_keys("\M-l", false) - assert_byte_pointer_size('{}#* aaa!!!ccc ') - assert_cursor(20) - assert_cursor_max(20) - assert_line('{}#* aaa!!!ccc ') + assert_line_around_cursor('{}#* aaa!!!ccc ', '') end def test_em_upper_case input_keys('AbC def{bBb}CCC') input_keys("\C-a\M-u", false) - assert_byte_pointer_size('ABC') - assert_cursor(3) - assert_cursor_max(15) - assert_line('ABC def{bBb}CCC') + assert_line_around_cursor('ABC', ' def{bBb}CCC') input_keys("\M-u", false) - assert_byte_pointer_size('ABC DEF') - assert_cursor(7) - assert_cursor_max(15) - assert_line('ABC DEF{bBb}CCC') + assert_line_around_cursor('ABC DEF', '{bBb}CCC') input_keys("\M-u", false) - assert_byte_pointer_size('ABC DEF{BBB') - assert_cursor(11) - assert_cursor_max(15) - assert_line('ABC DEF{BBB}CCC') + assert_line_around_cursor('ABC DEF{BBB', '}CCC') input_keys("\M-u", false) - assert_byte_pointer_size('ABC DEF{BBB}CCC') - assert_cursor(15) - assert_cursor_max(15) - assert_line('ABC DEF{BBB}CCC') + assert_line_around_cursor('ABC DEF{BBB}CCC', '') end def test_em_upper_case_with_complex_example input_keys('{}#* AaA!!!cCc ') input_keys("\C-a\M-u", false) - assert_byte_pointer_size('{}#* AAA') - assert_cursor(11) - assert_cursor_max(20) - assert_line('{}#* AAA!!!cCc ') + assert_line_around_cursor('{}#* AAA', '!!!cCc ') input_keys("\M-u", false) - assert_byte_pointer_size('{}#* AAA!!!CCC') - assert_cursor(17) - assert_cursor_max(20) - assert_line('{}#* AAA!!!CCC ') + assert_line_around_cursor('{}#* AAA!!!CCC', ' ') input_keys("\M-u", false) - assert_byte_pointer_size('{}#* AAA!!!CCC ') - assert_cursor(20) - assert_cursor_max(20) - assert_line('{}#* AAA!!!CCC ') + assert_line_around_cursor('{}#* AAA!!!CCC ', '') end def test_em_delete_or_list @@ -1286,28 +726,16 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase } } input_keys('fooo') - assert_byte_pointer_size('fooo') - assert_cursor(4) - assert_cursor_max(4) - assert_line('fooo') + assert_line_around_cursor('fooo', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-b", false) - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(4) - assert_line('fooo') + assert_line_around_cursor('foo', 'o') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) @line_editor.input_key(Reline::Key.new(:em_delete_or_list, :em_delete_or_list, false)) - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) @line_editor.input_key(Reline::Key.new(:em_delete_or_list, :em_delete_or_list, false)) - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') assert_equal(%w{foo_foo foo_bar foo_baz}, @line_editor.instance_variable_get(:@menu_info).list) end @@ -1322,22 +750,13 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase } } input_keys('foo_') - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(%w{foo_foo foo_bar}, @line_editor.instance_variable_get(:@menu_info).list) end @@ -1353,36 +772,63 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase } } input_keys('fo') - assert_byte_pointer_size('fo') - assert_cursor(2) - assert_cursor_max(2) - assert_line('fo') + assert_line_around_cursor('fo', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(%w{foo_foo foo_bar foo_baz}, @line_editor.instance_variable_get(:@menu_info).list) input_keys('a') input_keys("\C-i", false) - assert_byte_pointer_size('foo_a') - assert_cursor(5) - assert_cursor_max(5) - assert_line('foo_a') + assert_line_around_cursor('foo_a', '') input_keys("\C-h", false) input_keys('b') input_keys("\C-i", false) - assert_byte_pointer_size('foo_ba') - assert_cursor(6) - assert_cursor_max(6) - assert_line('foo_ba') + assert_line_around_cursor('foo_ba', '') + input_keys("\C-h") + input_key_by_symbol(:complete) + assert_line_around_cursor('foo_ba', '') + input_keys("\C-h", false) + input_key_by_symbol(:menu_complete) + assert_line_around_cursor('foo_bar', '') + input_key_by_symbol(:menu_complete) + assert_line_around_cursor('foo_baz', '') + input_keys("\C-h", false) + input_key_by_symbol(:menu_complete_backward) + assert_line_around_cursor('foo_baz', '') + input_key_by_symbol(:menu_complete_backward) + assert_line_around_cursor('foo_bar', '') + end + + def test_autocompletion + @config.autocompletion = true + @line_editor.completion_proc = proc { |word| + %w{ + Readline + Regexp + RegexpError + }.map { |i| + i.encode(@encoding) + } + } + input_keys('Re') + assert_line_around_cursor('Re', '') + input_keys("\C-i", false) + assert_line_around_cursor('Readline', '') + input_keys("\C-i", false) + assert_line_around_cursor('Regexp', '') + input_key_by_symbol(:completion_journey_up) + assert_line_around_cursor('Readline', '') + input_key_by_symbol(:complete) + assert_line_around_cursor('Regexp', '') + input_key_by_symbol(:menu_complete_backward) + assert_line_around_cursor('Readline', '') + input_key_by_symbol(:menu_complete) + assert_line_around_cursor('Regexp', '') + ensure + @config.autocompletion = false end def test_completion_with_indent @@ -1397,22 +843,13 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase } } input_keys(' fo') - assert_byte_pointer_size(' fo') - assert_cursor(4) - assert_cursor_max(4) - assert_line(' fo') + assert_line_around_cursor(' fo', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size(' foo_') - assert_cursor(6) - assert_cursor_max(6) - assert_line(' foo_') + assert_line_around_cursor(' foo_', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size(' foo_') - assert_cursor(6) - assert_cursor_max(6) - assert_line(' foo_') + assert_line_around_cursor(' foo_', '') assert_equal(%w{foo_foo foo_bar foo_baz}, @line_editor.instance_variable_get(:@menu_info).list) end @@ -1428,22 +865,13 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase } } input_keys(' "".fo') - assert_byte_pointer_size(' "".fo') - assert_cursor(7) - assert_cursor_max(7) - assert_line(' "".fo') + assert_line_around_cursor(' "".fo', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size(' "".foo_') - assert_cursor(9) - assert_cursor_max(9) - assert_line(' "".foo_') + assert_line_around_cursor(' "".foo_', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size(' "".foo_') - assert_cursor(9) - assert_cursor_max(9) - assert_line(' "".foo_') + assert_line_around_cursor(' "".foo_', '') assert_equal(%w{"".foo_foo "".foo_bar "".foo_baz}, @line_editor.instance_variable_get(:@menu_info).list) end @@ -1461,54 +889,33 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase matched = m } input_keys('fo') - assert_byte_pointer_size('fo') - assert_cursor(2) - assert_cursor_max(2) - assert_line('fo') + assert_line_around_cursor('fo', '') assert_equal(Reline::LineEditor::CompletionState::NORMAL, @line_editor.instance_variable_get(:@completion_state)) assert_equal(nil, matched) input_keys("\C-i", false) - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') assert_equal(Reline::LineEditor::CompletionState::MENU_WITH_PERFECT_MATCH, @line_editor.instance_variable_get(:@completion_state)) assert_equal(nil, matched) input_keys("\C-i", false) - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') assert_equal(Reline::LineEditor::CompletionState::PERFECT_MATCH, @line_editor.instance_variable_get(:@completion_state)) assert_equal(nil, matched) input_keys("\C-i", false) - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') assert_equal(Reline::LineEditor::CompletionState::PERFECT_MATCH, @line_editor.instance_variable_get(:@completion_state)) assert_equal('foo', matched) matched = nil input_keys('_') input_keys("\C-i", false) - assert_byte_pointer_size('foo_bar') - assert_cursor(7) - assert_cursor_max(7) - assert_line('foo_bar') + assert_line_around_cursor('foo_bar', '') assert_equal(Reline::LineEditor::CompletionState::MENU_WITH_PERFECT_MATCH, @line_editor.instance_variable_get(:@completion_state)) assert_equal(nil, matched) input_keys("\C-i", false) - assert_byte_pointer_size('foo_bar') - assert_cursor(7) - assert_cursor_max(7) - assert_line('foo_bar') + assert_line_around_cursor('foo_bar', '') assert_equal(Reline::LineEditor::CompletionState::PERFECT_MATCH, @line_editor.instance_variable_get(:@completion_state)) assert_equal(nil, matched) input_keys("\C-i", false) - assert_byte_pointer_size('foo_bar') - assert_cursor(7) - assert_cursor_max(7) - assert_line('foo_bar') + assert_line_around_cursor('foo_bar', '') assert_equal(Reline::LineEditor::CompletionState::PERFECT_MATCH, @line_editor.instance_variable_get(:@completion_state)) assert_equal('foo_bar', matched) end @@ -1525,43 +932,25 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase } } input_keys('fo') - assert_byte_pointer_size('fo') - assert_cursor(2) - assert_cursor_max(2) - assert_line('fo') + assert_line_around_cursor('fo', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(%w{foo_foo foo_bar}, @line_editor.instance_variable_get(:@menu_info).list) @config.completion_ignore_case = true input_keys("\C-i", false) - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(%w{foo_foo foo_bar Foo_baz}, @line_editor.instance_variable_get(:@menu_info).list) input_keys('a') input_keys("\C-i", false) - assert_byte_pointer_size('foo_a') - assert_cursor(5) - assert_cursor_max(5) - assert_line('foo_a') + assert_line_around_cursor('foo_a', '') input_keys("\C-h", false) input_keys('b') input_keys("\C-i", false) - assert_byte_pointer_size('foo_ba') - assert_cursor(6) - assert_cursor_max(6) - assert_line('foo_ba') + assert_line_around_cursor('foo_ba', '') end def test_completion_in_middle_of_line @@ -1576,17 +965,11 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase } } input_keys('abcde fo ABCDE') - assert_line('abcde fo ABCDE') + assert_line_around_cursor('abcde fo ABCDE', '') input_keys("\C-b" * 6 + "\C-i", false) - assert_byte_pointer_size('abcde foo_') - assert_cursor(10) - assert_cursor_max(16) - assert_line('abcde foo_ ABCDE') + assert_line_around_cursor('abcde foo_', ' ABCDE') input_keys("\C-b" * 2 + "\C-i", false) - assert_byte_pointer_size('abcde foo_') - assert_cursor(10) - assert_cursor_max(18) - assert_line('abcde foo_o_ ABCDE') + assert_line_around_cursor('abcde foo_', 'o_ ABCDE') end def test_completion_with_nil_value @@ -1602,125 +985,65 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase } @config.completion_ignore_case = true input_keys('fo') - assert_byte_pointer_size('fo') - assert_cursor(2) - assert_cursor_max(2) - assert_line('fo') + assert_line_around_cursor('fo', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(nil, @line_editor.instance_variable_get(:@menu_info)) input_keys("\C-i", false) - assert_byte_pointer_size('foo_') - assert_cursor(4) - assert_cursor_max(4) - assert_line('foo_') + assert_line_around_cursor('foo_', '') assert_equal(%w{foo_foo foo_bar Foo_baz}, @line_editor.instance_variable_get(:@menu_info).list) input_keys('a') input_keys("\C-i", false) - assert_byte_pointer_size('foo_a') - assert_cursor(5) - assert_cursor_max(5) - assert_line('foo_a') + assert_line_around_cursor('foo_a', '') input_keys("\C-h", false) input_keys('b') input_keys("\C-i", false) - assert_byte_pointer_size('foo_ba') - assert_cursor(6) - assert_cursor_max(6) - assert_line('foo_ba') + assert_line_around_cursor('foo_ba', '') end def test_em_kill_region input_keys('abc def{bbb}ccc ddd ') - assert_byte_pointer_size('abc def{bbb}ccc ddd ') - assert_cursor(26) - assert_cursor_max(26) - assert_line('abc def{bbb}ccc ddd ') + assert_line_around_cursor('abc def{bbb}ccc ddd ', '') input_keys("\C-w", false) - assert_byte_pointer_size('abc def{bbb}ccc ') - assert_cursor(20) - assert_cursor_max(20) - assert_line('abc def{bbb}ccc ') + assert_line_around_cursor('abc def{bbb}ccc ', '') input_keys("\C-w", false) - assert_byte_pointer_size('abc ') - assert_cursor(6) - assert_cursor_max(6) - assert_line('abc ') + assert_line_around_cursor('abc ', '') input_keys("\C-w", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys("\C-w", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_em_kill_region_mbchar input_keys('あ い う{う}う ') - assert_byte_pointer_size('あ い う{う}う ') - assert_cursor(21) - assert_cursor_max(21) - assert_line('あ い う{う}う ') + assert_line_around_cursor('あ い う{う}う ', '') input_keys("\C-w", false) - assert_byte_pointer_size('あ い ') - assert_cursor(10) - assert_cursor_max(10) - assert_line('あ い ') + assert_line_around_cursor('あ い ', '') input_keys("\C-w", false) - assert_byte_pointer_size('あ ') - assert_cursor(5) - assert_cursor_max(5) - assert_line('あ ') + assert_line_around_cursor('あ ', '') input_keys("\C-w", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_vi_search_prev Reline::HISTORY.concat(%w{abc 123 AAA}) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-ra\C-j") - assert_line('abc') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) + assert_line_around_cursor('', 'abc') end def test_larger_histories_than_history_size history_size = @config.history_size @config.history_size = 2 Reline::HISTORY.concat(%w{abc 123 AAA}) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-p") - assert_line('AAA') - assert_byte_pointer_size('AAA') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('AAA', '') input_keys("\C-p") - assert_line('123') - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('123', '') input_keys("\C-p") - assert_line('123') - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('123', '') ensure @config.history_size = history_size end @@ -1731,25 +1054,13 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase '12aa', '1234' # new ]) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-r123") - assert_line('1234') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) # doesn't determine yet + assert_line_around_cursor('1234', '') input_keys("\C-ha") - assert_line('12aa') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('12aa', '') input_keys("\C-h3") - assert_line('1235') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('1235', '') end def test_search_history_to_front @@ -1758,25 +1069,13 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase '12aa', '1234' # new ]) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-s123") - assert_line('1235') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) # doesn't determine yet + assert_line_around_cursor('1235', '') input_keys("\C-ha") - assert_line('12aa') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('12aa', '') input_keys("\C-h3") - assert_line('1234') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('1234', '') end def test_search_history_front_and_back @@ -1785,30 +1084,15 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase '12aa', '1234' # new ]) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-s12") - assert_line('1235') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) # doesn't determine yet + assert_line_around_cursor('1235', '') input_keys("\C-s") - assert_line('12aa') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('12aa', '') input_keys("\C-r") - assert_line('12aa') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('12aa', '') input_keys("\C-r") - assert_line('1235') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('1235', '') end def test_search_history_back_and_front @@ -1817,30 +1101,15 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase '12aa', '1234' # new ]) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-r12") - assert_line('1234') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) # doesn't determine yet + assert_line_around_cursor('1234', '') input_keys("\C-r") - assert_line('12aa') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('12aa', '') input_keys("\C-s") - assert_line('12aa') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('12aa', '') input_keys("\C-s") - assert_line('1234') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('1234', '') end def test_search_history_to_back_in_the_middle_of_histories @@ -1849,20 +1118,11 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase '12aa', '1234' # new ]) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-p\C-p") - assert_line('12aa') - assert_byte_pointer_size('12aa') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('12aa', '') input_keys("\C-r123") - assert_line('1235') - assert_byte_pointer_size('1235') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('1235', '') end def test_search_history_twice @@ -1871,20 +1131,11 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase '12aa', '1234' # new ]) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-r123") - assert_line('1234') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) # doesn't determine yet + assert_line_around_cursor('1234', '') input_keys("\C-r") - assert_line('1235') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('1235', '') end def test_search_history_by_last_determined @@ -1893,35 +1144,17 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase '12aa', '1234' # new ]) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-r123") - assert_line('1234') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) # doesn't determine yet + assert_line_around_cursor('1234', '') input_keys("\C-j") - assert_line('1234') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(4) + assert_line_around_cursor('', '1234') input_keys("\C-k") # delete - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-r") - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-r") - assert_line('1235') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('1235', '') end def test_search_history_with_isearch_terminator @@ -1933,76 +1166,40 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase '12aa', '1234' # new ]) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys("\C-r12a") - assert_line('12aa') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) # doesn't determine yet + assert_line_around_cursor('12aa', '') input_keys('Y') - assert_line('12aa') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(4) + assert_line_around_cursor('', '12aa') input_keys('x') - assert_line('x12aa') - assert_byte_pointer_size('x') - assert_cursor(1) - assert_cursor_max(5) + assert_line_around_cursor('x', '12aa') end def test_em_set_mark_and_em_exchange_mark input_keys('aaa bbb ccc ddd') - assert_byte_pointer_size('aaa bbb ccc ddd') - assert_cursor(15) - assert_cursor_max(15) - assert_line('aaa bbb ccc ddd') + assert_line_around_cursor('aaa bbb ccc ddd', '') input_keys("\C-a\M-F\M-F", false) - assert_byte_pointer_size('aaa bbb') - assert_cursor(7) - assert_cursor_max(15) - assert_line('aaa bbb ccc ddd') + assert_line_around_cursor('aaa bbb', ' ccc ddd') assert_equal(nil, @line_editor.instance_variable_get(:@mark_pointer)) input_keys("\x00", false) # C-Space - assert_byte_pointer_size('aaa bbb') - assert_cursor(7) - assert_cursor_max(15) - assert_line('aaa bbb ccc ddd') + assert_line_around_cursor('aaa bbb', ' ccc ddd') assert_equal([7, 0], @line_editor.instance_variable_get(:@mark_pointer)) input_keys("\C-a", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(15) - assert_line('aaa bbb ccc ddd') + assert_line_around_cursor('', 'aaa bbb ccc ddd') assert_equal([7, 0], @line_editor.instance_variable_get(:@mark_pointer)) input_key_by_symbol(:em_exchange_mark) - assert_byte_pointer_size('aaa bbb') - assert_cursor(7) - assert_cursor_max(15) - assert_line('aaa bbb ccc ddd') + assert_line_around_cursor('aaa bbb', ' ccc ddd') assert_equal([0, 0], @line_editor.instance_variable_get(:@mark_pointer)) end def test_em_exchange_mark_without_mark input_keys('aaa bbb ccc ddd') - assert_byte_pointer_size('aaa bbb ccc ddd') - assert_cursor(15) - assert_cursor_max(15) - assert_line('aaa bbb ccc ddd') + assert_line_around_cursor('aaa bbb ccc ddd', '') input_keys("\C-a\M-f", false) - assert_byte_pointer_size('aaa') - assert_cursor(3) - assert_cursor_max(15) - assert_line('aaa bbb ccc ddd') + assert_line_around_cursor('aaa', ' bbb ccc ddd') assert_equal(nil, @line_editor.instance_variable_get(:@mark_pointer)) input_key_by_symbol(:em_exchange_mark) - assert_byte_pointer_size('aaa') - assert_cursor(3) - assert_cursor_max(15) - assert_line('aaa bbb ccc ddd') + assert_line_around_cursor('aaa', ' bbb ccc ddd') assert_equal(nil, @line_editor.instance_variable_get(:@mark_pointer)) end @@ -2013,7 +1210,7 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase $VERBOSE = verbose @line_editor.output_modifier_proc = proc { |output| Reline::Unicode.escape_for_print(output) } input_keys("abcdef\n") - result = @line_editor.__send__(:modify_lines, @line_editor.whole_lines) + result = @line_editor.__send__(:modify_lines, @line_editor.whole_lines, @line_editor.finished?) $/ = nil assert_equal(['abcdef'], result) ensure @@ -2031,20 +1228,11 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase input_keys('123') # The ed_search_prev_history doesn't have default binding @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(5) - assert_line('12345') + assert_line_around_cursor('123', '45') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(5) - assert_line('12356') + assert_line_around_cursor('123', '56') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(5) - assert_line('12356') + assert_line_around_cursor('123', '56') end def test_ed_search_prev_history_with_empty @@ -2055,25 +1243,13 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase ]) # The ed_search_prev_history doesn't have default binding @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) - assert_line('12345') + assert_line_around_cursor('', '12345') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) - assert_line('12aaa') + assert_line_around_cursor('', '12aaa') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) - assert_line('12356') + assert_line_around_cursor('', '12356') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) - assert_line('12356') + assert_line_around_cursor('', '12356') end def test_ed_search_prev_history_without_match @@ -2085,10 +1261,7 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase input_keys('ABC') # The ed_search_prev_history doesn't have default binding @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('ABC') - assert_cursor(3) - assert_cursor_max(3) - assert_line('ABC') + assert_line_around_cursor('ABC', '') end def test_ed_search_next_history @@ -2100,30 +1273,15 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase input_keys('123') # The ed_search_prev_history and ed_search_next_history doesn't have default binding @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(5) - assert_line('12345') + assert_line_around_cursor('123', '45') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(5) - assert_line('12356') + assert_line_around_cursor('123', '56') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(5) - assert_line('12356') + assert_line_around_cursor('123', '56') @line_editor.__send__(:ed_search_next_history, "\C-n".ord) - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(5) - assert_line('12345') + assert_line_around_cursor('123', '45') @line_editor.__send__(:ed_search_next_history, "\C-n".ord) - assert_byte_pointer_size('123') - assert_cursor(3) - assert_cursor_max(5) - assert_line('12345') + assert_line_around_cursor('123', '45') end def test_ed_search_next_history_with_empty @@ -2134,35 +1292,25 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase ]) # The ed_search_prev_history and ed_search_next_history doesn't have default binding @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) - assert_line('12345') + assert_line_around_cursor('', '12345') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) - assert_line('12aaa') + assert_line_around_cursor('', '12aaa') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) - assert_line('12356') + assert_line_around_cursor('', '12356') @line_editor.__send__(:ed_search_next_history, "\C-n".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) - assert_line('12aaa') + assert_line_around_cursor('', '12aaa') @line_editor.__send__(:ed_search_next_history, "\C-n".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) - assert_line('12345') + assert_line_around_cursor('', '12345') @line_editor.__send__(:ed_search_next_history, "\C-n".ord) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') + end + + def test_incremental_search_history_cancel_by_symbol_key + # ed_prev_char should move cursor left and cancel incremental search + input_keys("abc\C-r") + input_key_by_symbol(:ed_prev_char) + input_keys('d') + assert_line_around_cursor('abd', 'c') end # Unicode emoji test @@ -2170,97 +1318,49 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase omit "This test is for UTF-8 but the locale is #{Reline.core.encoding}" if Reline.core.encoding != Encoding::UTF_8 # U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 is family: man, woman, girl, boy "👨👩👧👦" input_keys("\u{1F468}") # U+1F468 is man "👨" - assert_line("\u{1F468}") - assert_byte_pointer_size("\u{1F468}") - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('👨', '') input_keys("\u200D") # U+200D is ZERO WIDTH JOINER - assert_line("\u{1F468 200D}") - assert_byte_pointer_size("\u{1F468 200D}") - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('👨', '') input_keys("\u{1F469}") # U+1F469 is woman "👩" - assert_line("\u{1F468 200D 1F469}") - assert_byte_pointer_size("\u{1F468 200D 1F469}") - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('👨👩', '') input_keys("\u200D") # U+200D is ZERO WIDTH JOINER - assert_line("\u{1F468 200D 1F469 200D}") - assert_byte_pointer_size("\u{1F468 200D 1F469 200D}") - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('👨👩', '') input_keys("\u{1F467}") # U+1F467 is girl "👧" - assert_line("\u{1F468 200D 1F469 200D 1F467}") - assert_byte_pointer_size("\u{1F468 200D 1F469 200D 1F467}") - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('👨👩👧', '') input_keys("\u200D") # U+200D is ZERO WIDTH JOINER - assert_line("\u{1F468 200D 1F469 200D 1F467 200D}") - assert_byte_pointer_size("\u{1F468 200D 1F469 200D 1F467 200D}") - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('👨👩👧', '') input_keys("\u{1F466}") # U+1F466 is boy "👦" - assert_line("\u{1F468 200D 1F469 200D 1F467 200D 1F466}") - assert_byte_pointer_size("\u{1F468 200D 1F469 200D 1F467 200D 1F466}") - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('👨👩👧👦', '') # U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 is family: man, woman, girl, boy "👨👩👧👦" input_keys("\u{1F468 200D 1F469 200D 1F467 200D 1F466}") - assert_line("\u{1F468 200D 1F469 200D 1F467 200D 1F466 1F468 200D 1F469 200D 1F467 200D 1F466}") - assert_byte_pointer_size("\u{1F468 200D 1F469 200D 1F467 200D 1F466 1F468 200D 1F469 200D 1F467 200D 1F466}") - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('👨👩👧👦👨👩👧👦', '') end def test_ed_insert_for_include_valiation_selector omit "This test is for UTF-8 but the locale is #{Reline.core.encoding}" if Reline.core.encoding != Encoding::UTF_8 # U+0030 U+FE00 is DIGIT ZERO + VARIATION SELECTOR-1 "0︀" input_keys("\u0030") # U+0030 is DIGIT ZERO - assert_line("\u0030") - assert_byte_pointer_size("\u0030") - assert_cursor(1) - assert_cursor_max(1) + assert_line_around_cursor('0', '') input_keys("\uFE00") # U+FE00 is VARIATION SELECTOR-1 - assert_line("\u{0030 FE00}") - assert_byte_pointer_size("\u{0030 FE00}") - assert_cursor(1) - assert_cursor_max(1) + assert_line_around_cursor('0︀', '') end def test_em_yank_pop input_keys("def hoge\C-w\C-b\C-f\C-w", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys("\C-y", false) - assert_byte_pointer_size('def ') - assert_cursor(4) - assert_cursor_max(4) - assert_line('def ') + assert_line_around_cursor('def ', '') input_keys("\M-\C-y", false) - assert_byte_pointer_size('hoge') - assert_cursor(4) - assert_cursor_max(4) - assert_line('hoge') + assert_line_around_cursor('hoge', '') end def test_em_kill_region_with_kill_ring input_keys("def hoge\C-b\C-b\C-b\C-b", false) - assert_byte_pointer_size('def ') - assert_cursor(4) - assert_cursor_max(8) - assert_line('def hoge') + assert_line_around_cursor('def ', 'hoge') input_keys("\C-k\C-w", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys("\C-y", false) - assert_byte_pointer_size('def hoge') - assert_cursor(8) - assert_cursor_max(8) - assert_line('def hoge') + assert_line_around_cursor('def hoge', '') end def test_ed_search_prev_next_history_in_multibyte @@ -2276,104 +1376,133 @@ class Reline::KeyActor::Emacs::Test < Reline::TestCase assert_whole_lines(['def foo', ' 12345', 'end']) assert_line_index(1) assert_whole_lines(['def foo', ' 12345', 'end']) - assert_byte_pointer_size(' 123') - assert_cursor(5) - assert_cursor_max(7) - assert_line(' 12345') + assert_line_around_cursor(' 123', '45') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) assert_line_index(2) assert_whole_lines(['def hoge', ' 67890', ' 12345', 'end']) - assert_byte_pointer_size(' 123') - assert_cursor(5) - assert_cursor_max(7) - assert_line(' 12345') + assert_line_around_cursor(' 123', '45') @line_editor.__send__(:ed_search_prev_history, "\C-p".ord) assert_line_index(2) assert_whole_lines(['def hoge', ' 67890', ' 12345', 'end']) - assert_byte_pointer_size(' 123') - assert_cursor(5) - assert_cursor_max(7) - assert_line(' 12345') + assert_line_around_cursor(' 123', '45') @line_editor.__send__(:ed_search_next_history, "\C-n".ord) assert_line_index(1) assert_whole_lines(['def foo', ' 12345', 'end']) - assert_byte_pointer_size(' 123') - assert_cursor(5) - assert_cursor_max(7) - assert_line(' 12345') + assert_line_around_cursor(' 123', '45') @line_editor.__send__(:ed_search_next_history, "\C-n".ord) assert_line_index(1) assert_whole_lines(['def foo', ' 12345', 'end']) - assert_byte_pointer_size(' 123') - assert_cursor(5) - assert_cursor_max(7) - assert_line(' 12345') + assert_line_around_cursor(' 123', '45') end def test_ignore_NUL_by_ed_quoted_insert input_keys(%Q{"\C-v\C-@"}, false) - assert_byte_pointer_size('""') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('""', '') end def test_ed_argument_digit_by_meta_num input_keys('abcdef') - assert_byte_pointer_size('abcdef') - assert_cursor(6) - assert_cursor_max(6) - assert_line('abcdef') + assert_line_around_cursor('abcdef', '') input_keys("\M-2", false) input_keys("\C-h", false) - assert_byte_pointer_size('abcd') - assert_cursor(4) - assert_cursor_max(4) - assert_line('abcd') + assert_line_around_cursor('abcd', '') end def test_halfwidth_kana_width_dakuten input_raw_keys('ガギゲゴ') - assert_byte_pointer_size('ガギゲゴ') - assert_cursor(8) - assert_cursor_max(8) + assert_line_around_cursor('ガギゲゴ', '') input_keys("\C-b\C-b", false) - assert_byte_pointer_size('ガギ') - assert_cursor(4) - assert_cursor_max(8) + assert_line_around_cursor('ガギ', 'ゲゴ') input_raw_keys('グ', false) - assert_byte_pointer_size('ガギグ') - assert_cursor(6) - assert_cursor_max(10) - assert_line('ガギグゲゴ') + assert_line_around_cursor('ガギグ', 'ゲゴ') end def test_input_unknown_char input_keys('') # U+0378 (unassigned) - assert_line('') - assert_byte_pointer_size('') - assert_cursor(1) - assert_cursor_max(1) + assert_line_around_cursor('', '') end def test_unix_line_discard input_keys("\C-u", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('abc') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abc', '') input_keys("\C-b\C-u", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(1) - assert_line('c') + assert_line_around_cursor('', 'c') input_keys("\C-f\C-u", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') + end + + def test_vi_editing_mode + @line_editor.__send__(:vi_editing_mode, nil) + assert(@config.editing_mode_is?(:vi_insert)) + end + + def test_undo + input_keys("\C-_", false) + assert_line_around_cursor('', '') + input_keys("aあb\C-h\C-h\C-h", false) + assert_line_around_cursor('', '') + input_keys("\C-_", false) + assert_line_around_cursor('a', '') + input_keys("\C-_", false) + assert_line_around_cursor('aあ', '') + input_keys("\C-_", false) + assert_line_around_cursor('aあb', '') + input_keys("\C-_", false) + assert_line_around_cursor('aあ', '') + input_keys("\C-_", false) + assert_line_around_cursor('a', '') + input_keys("\C-_", false) + assert_line_around_cursor('', '') + end + + def test_undo_with_cursor_position + input_keys("abc\C-b\C-h", false) + assert_line_around_cursor('a', 'c') + input_keys("\C-_", false) + assert_line_around_cursor('ab', 'c') + input_keys("あいう\C-b\C-h", false) + assert_line_around_cursor('abあ', 'うc') + input_keys("\C-_", false) + assert_line_around_cursor('abあい', 'うc') + end + + def test_undo_with_multiline + @line_editor.multiline_on + @line_editor.confirm_multiline_termination_proc = proc {} + input_keys("1\n2\n3", false) + assert_whole_lines(["1", "2", "3"]) + assert_line_index(2) + assert_line_around_cursor('3', '') + input_keys("\C-p\C-h\C-h", false) + assert_whole_lines(["1", "3"]) + assert_line_index(0) + assert_line_around_cursor('1', '') + input_keys("\C-_", false) + assert_whole_lines(["1", "", "3"]) + assert_line_index(1) + assert_line_around_cursor('', '') + input_keys("\C-_", false) + assert_whole_lines(["1", "2", "3"]) + assert_line_index(1) + assert_line_around_cursor('2', '') + input_keys("\C-_", false) + assert_whole_lines(["1", "2", ""]) + assert_line_index(2) + assert_line_around_cursor('', '') + input_keys("\C-_", false) + assert_whole_lines(["1", "2"]) + assert_line_index(1) + assert_line_around_cursor('2', '') + end + + def test_undo_with_many_times + str = "a" + "b" * 100 + input_keys(str, false) + 100.times { input_keys("\C-_", false) } + assert_line_around_cursor('a', '') + input_keys("\C-_", false) + assert_line_around_cursor('a', '') end end diff --git a/test/reline/test_key_actor_vi.rb b/test/reline/test_key_actor_vi.rb index 20630e5809..4deae2dd83 100644 --- a/test/reline/test_key_actor_vi.rb +++ b/test/reline/test_key_actor_vi.rb @@ -25,950 +25,513 @@ class Reline::KeyActor::ViInsert::Test < Reline::TestCase def test_vi_command_mode_with_input input_keys("abc\C-[") assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) - assert_line('abc') + assert_line_around_cursor('ab', 'c') end def test_vi_insert assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) input_keys('i') - assert_line('i') - assert_cursor(1) + assert_line_around_cursor('i', '') assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) input_keys("\C-[") - assert_line('i') - assert_cursor(0) + assert_line_around_cursor('', 'i') assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) input_keys('i') - assert_line('i') - assert_cursor(0) + assert_line_around_cursor('', 'i') assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) end def test_vi_add assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) input_keys('a') - assert_line('a') - assert_cursor(1) + assert_line_around_cursor('a', '') assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) input_keys("\C-[") - assert_line('a') - assert_cursor(0) + assert_line_around_cursor('', 'a') assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) input_keys('a') - assert_line('a') - assert_cursor(1) + assert_line_around_cursor('a', '') assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) end def test_vi_insert_at_bol input_keys('I') - assert_line('I') + assert_line_around_cursor('I', '') assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) input_keys("12345\C-[hh") - assert_line('I12345') - assert_byte_pointer_size('I12') - assert_cursor(3) - assert_cursor_max(6) + assert_line_around_cursor('I12', '345') assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) input_keys('I') - assert_line('I12345') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'I12345') assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) end def test_vi_add_at_eol input_keys('A') - assert_line('A') + assert_line_around_cursor('A', '') assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) input_keys("12345\C-[hh") - assert_line('A12345') - assert_byte_pointer_size('A12') - assert_cursor(3) - assert_cursor_max(6) + assert_line_around_cursor('A12', '345') assert_instance_of(Reline::KeyActor::ViCommand, @config.editing_mode) input_keys('A') - assert_line('A12345') - assert_byte_pointer_size('A12345') - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor('A12345', '') assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) end def test_ed_insert_one input_keys('a') - assert_line('a') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(1) + assert_line_around_cursor('a', '') end def test_ed_insert_two input_keys('ab') - assert_line('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') end def test_ed_insert_mbchar_one input_keys('か') - assert_line('か') - assert_byte_pointer_size('か') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('か', '') end def test_ed_insert_mbchar_two input_keys('かき') - assert_line('かき') - assert_byte_pointer_size('かき') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('かき', '') end def test_ed_insert_for_mbchar_by_plural_code_points input_keys("か\u3099") - assert_line("か\u3099") - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor("か\u3099", '') end def test_ed_insert_for_plural_mbchar_by_plural_code_points input_keys("か\u3099き\u3099") - assert_line("か\u3099き\u3099") - assert_byte_pointer_size("か\u3099き\u3099") - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor("か\u3099き\u3099", '') end def test_ed_next_char input_keys("abcdef\C-[0") - assert_line('abcdef') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'abcdef') input_keys('l') - assert_line('abcdef') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(6) + assert_line_around_cursor('a', 'bcdef') input_keys('2l') - assert_line('abcdef') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(6) + assert_line_around_cursor('abc', 'def') end def test_ed_prev_char input_keys("abcdef\C-[") - assert_line('abcdef') - assert_byte_pointer_size('abcde') - assert_cursor(5) - assert_cursor_max(6) + assert_line_around_cursor('abcde', 'f') input_keys('h') - assert_line('abcdef') - assert_byte_pointer_size('abcd') - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor('abcd', 'ef') input_keys('2h') - assert_line('abcdef') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(6) + assert_line_around_cursor('ab', 'cdef') end def test_history Reline::HISTORY.concat(%w{abc 123 AAA}) input_keys("\C-[") - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys('k') - assert_line('AAA') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) + assert_line_around_cursor('', 'AAA') input_keys('2k') - assert_line('abc') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) + assert_line_around_cursor('', 'abc') input_keys('j') - assert_line('123') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) + assert_line_around_cursor('', '123') input_keys('2j') - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') end def test_vi_paste_prev input_keys("abcde\C-[3h") - assert_line('abcde') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(5) + assert_line_around_cursor('a', 'bcde') input_keys('P') - assert_line('abcde') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(5) + assert_line_around_cursor('a', 'bcde') input_keys('d$') - assert_line('a') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(1) + assert_line_around_cursor('', 'a') input_keys('P') - assert_line('bcdea') - assert_byte_pointer_size('bcd') - assert_cursor(3) - assert_cursor_max(5) + assert_line_around_cursor('bcd', 'ea') input_keys('2P') - assert_line('bcdbcdbcdeeea') - assert_byte_pointer_size('bcdbcdbcd') - assert_cursor(9) - assert_cursor_max(13) + assert_line_around_cursor('bcdbcdbcd', 'eeea') end def test_vi_paste_next input_keys("abcde\C-[3h") - assert_line('abcde') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(5) + assert_line_around_cursor('a', 'bcde') input_keys('p') - assert_line('abcde') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(5) + assert_line_around_cursor('a', 'bcde') input_keys('d$') - assert_line('a') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(1) + assert_line_around_cursor('', 'a') input_keys('p') - assert_line('abcde') - assert_byte_pointer_size('abcd') - assert_cursor(4) - assert_cursor_max(5) + assert_line_around_cursor('abcd', 'e') input_keys('2p') - assert_line('abcdebcdebcde') - assert_byte_pointer_size('abcdebcdebcd') - assert_cursor(12) - assert_cursor_max(13) + assert_line_around_cursor('abcdebcdebcd', 'e') end def test_vi_paste_prev_for_mbchar input_keys("あいうえお\C-[3h") - assert_line('あいうえお') - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor('あ', 'いうえお') input_keys('P') - assert_line('あいうえお') - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor('あ', 'いうえお') input_keys('d$') - assert_line('あ') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) + assert_line_around_cursor('', 'あ') input_keys('P') - assert_line('いうえおあ') - assert_byte_pointer_size('いうえ') - assert_cursor(6) - assert_cursor_max(10) + assert_line_around_cursor('いうえ', 'おあ') input_keys('2P') - assert_line('いうえいうえいうえおおおあ') - assert_byte_pointer_size('いうえいうえいうえ') - assert_cursor(18) - assert_cursor_max(26) + assert_line_around_cursor('いうえいうえいうえ', 'おおおあ') end def test_vi_paste_next_for_mbchar input_keys("あいうえお\C-[3h") - assert_line('あいうえお') - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor('あ', 'いうえお') input_keys('p') - assert_line('あいうえお') - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor('あ', 'いうえお') input_keys('d$') - assert_line('あ') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) + assert_line_around_cursor('', 'あ') input_keys('p') - assert_line('あいうえお') - assert_byte_pointer_size('あいうえ') - assert_cursor(8) - assert_cursor_max(10) + assert_line_around_cursor('あいうえ', 'お') input_keys('2p') - assert_line('あいうえおいうえおいうえお') - assert_byte_pointer_size('あいうえおいうえおいうえ') - assert_cursor(24) - assert_cursor_max(26) + assert_line_around_cursor('あいうえおいうえおいうえ', 'お') end def test_vi_paste_prev_for_mbchar_by_plural_code_points input_keys("か\u3099き\u3099く\u3099け\u3099こ\u3099\C-[3h") - assert_line("か\u3099き\u3099く\u3099け\u3099こ\u3099") - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor("か\u3099", "き\u3099く\u3099け\u3099こ\u3099") input_keys('P') - assert_line("か\u3099き\u3099く\u3099け\u3099こ\u3099") - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor("か\u3099", "き\u3099く\u3099け\u3099こ\u3099") input_keys('d$') - assert_line("か\u3099") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) + assert_line_around_cursor('', "か\u3099") input_keys('P') - assert_line("き\u3099く\u3099け\u3099こ\u3099か\u3099") - assert_byte_pointer_size("き\u3099く\u3099け\u3099") - assert_cursor(6) - assert_cursor_max(10) + assert_line_around_cursor("き\u3099く\u3099け\u3099", "こ\u3099か\u3099") input_keys('2P') - assert_line("き\u3099く\u3099け\u3099き\u3099く\u3099け\u3099き\u3099く\u3099け\u3099こ\u3099こ\u3099こ\u3099か\u3099") - assert_byte_pointer_size("き\u3099く\u3099け\u3099き\u3099く\u3099け\u3099き\u3099く\u3099け\u3099") - assert_cursor(18) - assert_cursor_max(26) + assert_line_around_cursor("き\u3099く\u3099け\u3099き\u3099く\u3099け\u3099き\u3099く\u3099け\u3099", "こ\u3099こ\u3099こ\u3099か\u3099") end def test_vi_paste_next_for_mbchar_by_plural_code_points input_keys("か\u3099き\u3099く\u3099け\u3099こ\u3099\C-[3h") - assert_line("か\u3099き\u3099く\u3099け\u3099こ\u3099") - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor("か\u3099", "き\u3099く\u3099け\u3099こ\u3099") input_keys('p') - assert_line("か\u3099き\u3099く\u3099け\u3099こ\u3099") - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor("か\u3099", "き\u3099く\u3099け\u3099こ\u3099") input_keys('d$') - assert_line("か\u3099") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) + assert_line_around_cursor('', "か\u3099") input_keys('p') - assert_line("か\u3099き\u3099く\u3099け\u3099こ\u3099") - assert_byte_pointer_size("か\u3099き\u3099く\u3099け\u3099") - assert_cursor(8) - assert_cursor_max(10) + assert_line_around_cursor("か\u3099き\u3099く\u3099け\u3099", "こ\u3099") input_keys('2p') - assert_line("か\u3099き\u3099く\u3099け\u3099こ\u3099き\u3099く\u3099け\u3099こ\u3099き\u3099く\u3099け\u3099こ\u3099") - assert_byte_pointer_size("か\u3099き\u3099く\u3099け\u3099こ\u3099き\u3099く\u3099け\u3099こ\u3099き\u3099く\u3099け\u3099") - assert_cursor(24) - assert_cursor_max(26) + assert_line_around_cursor("か\u3099き\u3099く\u3099け\u3099こ\u3099き\u3099く\u3099け\u3099こ\u3099き\u3099く\u3099け\u3099", "こ\u3099") end def test_vi_prev_next_word input_keys("aaa b{b}b ccc\C-[0") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(13) + assert_line_around_cursor('', 'aaa b{b}b ccc') input_keys('w') - assert_byte_pointer_size('aaa ') - assert_cursor(4) - assert_cursor_max(13) + assert_line_around_cursor('aaa ', 'b{b}b ccc') input_keys('w') - assert_byte_pointer_size('aaa b') - assert_cursor(5) - assert_cursor_max(13) + assert_line_around_cursor('aaa b', '{b}b ccc') input_keys('w') - assert_byte_pointer_size('aaa b{') - assert_cursor(6) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{', 'b}b ccc') input_keys('w') - assert_byte_pointer_size('aaa b{b') - assert_cursor(7) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b', '}b ccc') input_keys('w') - assert_byte_pointer_size('aaa b{b}') - assert_cursor(8) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}', 'b ccc') input_keys('w') - assert_byte_pointer_size('aaa b{b}b ') - assert_cursor(10) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b ', 'ccc') input_keys('w') - assert_byte_pointer_size('aaa b{b}b cc') - assert_cursor(12) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b cc', 'c') input_keys('b') - assert_byte_pointer_size('aaa b{b}b ') - assert_cursor(10) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b ', 'ccc') input_keys('b') - assert_byte_pointer_size('aaa b{b}') - assert_cursor(8) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}', 'b ccc') input_keys('b') - assert_byte_pointer_size('aaa b{b') - assert_cursor(7) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b', '}b ccc') input_keys('b') - assert_byte_pointer_size('aaa b{') - assert_cursor(6) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{', 'b}b ccc') input_keys('b') - assert_byte_pointer_size('aaa b') - assert_cursor(5) - assert_cursor_max(13) + assert_line_around_cursor('aaa b', '{b}b ccc') input_keys('b') - assert_byte_pointer_size('aaa ') - assert_cursor(4) - assert_cursor_max(13) + assert_line_around_cursor('aaa ', 'b{b}b ccc') input_keys('b') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(13) + assert_line_around_cursor('', 'aaa b{b}b ccc') input_keys('3w') - assert_byte_pointer_size('aaa b{') - assert_cursor(6) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{', 'b}b ccc') input_keys('3w') - assert_byte_pointer_size('aaa b{b}b ') - assert_cursor(10) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b ', 'ccc') input_keys('3w') - assert_byte_pointer_size('aaa b{b}b cc') - assert_cursor(12) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b cc', 'c') input_keys('3b') - assert_byte_pointer_size('aaa b{b') - assert_cursor(7) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b', '}b ccc') input_keys('3b') - assert_byte_pointer_size('aaa ') - assert_cursor(4) - assert_cursor_max(13) + assert_line_around_cursor('aaa ', 'b{b}b ccc') input_keys('3b') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(13) + assert_line_around_cursor('', 'aaa b{b}b ccc') end def test_vi_end_word input_keys("aaa b{b}}}b ccc\C-[0") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(19) + assert_line_around_cursor('', 'aaa b{b}}}b ccc') input_keys('e') - assert_byte_pointer_size('aa') - assert_cursor(2) - assert_cursor_max(19) + assert_line_around_cursor('aa', 'a b{b}}}b ccc') input_keys('e') - assert_byte_pointer_size('aaa ') - assert_cursor(6) - assert_cursor_max(19) + assert_line_around_cursor('aaa ', 'b{b}}}b ccc') input_keys('e') - assert_byte_pointer_size('aaa b') - assert_cursor(7) - assert_cursor_max(19) + assert_line_around_cursor('aaa b', '{b}}}b ccc') input_keys('e') - assert_byte_pointer_size('aaa b{') - assert_cursor(8) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{', 'b}}}b ccc') input_keys('e') - assert_byte_pointer_size('aaa b{b}}') - assert_cursor(11) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{b}}', '}b ccc') input_keys('e') - assert_byte_pointer_size('aaa b{b}}}') - assert_cursor(12) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{b}}}', 'b ccc') input_keys('e') - assert_byte_pointer_size('aaa b{b}}}b cc') - assert_cursor(18) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{b}}}b cc', 'c') input_keys('e') - assert_byte_pointer_size('aaa b{b}}}b cc') - assert_cursor(18) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{b}}}b cc', 'c') input_keys('03e') - assert_byte_pointer_size('aaa b') - assert_cursor(7) - assert_cursor_max(19) + assert_line_around_cursor('aaa b', '{b}}}b ccc') input_keys('3e') - assert_byte_pointer_size('aaa b{b}}}') - assert_cursor(12) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{b}}}', 'b ccc') input_keys('3e') - assert_byte_pointer_size('aaa b{b}}}b cc') - assert_cursor(18) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{b}}}b cc', 'c') end def test_vi_prev_next_big_word input_keys("aaa b{b}b ccc\C-[0") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(13) + assert_line_around_cursor('', 'aaa b{b}b ccc') input_keys('W') - assert_byte_pointer_size('aaa ') - assert_cursor(4) - assert_cursor_max(13) + assert_line_around_cursor('aaa ', 'b{b}b ccc') input_keys('W') - assert_byte_pointer_size('aaa b{b}b ') - assert_cursor(10) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b ', 'ccc') input_keys('W') - assert_byte_pointer_size('aaa b{b}b cc') - assert_cursor(12) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b cc', 'c') input_keys('B') - assert_byte_pointer_size('aaa b{b}b ') - assert_cursor(10) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b ', 'ccc') input_keys('B') - assert_byte_pointer_size('aaa ') - assert_cursor(4) - assert_cursor_max(13) + assert_line_around_cursor('aaa ', 'b{b}b ccc') input_keys('B') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(13) + assert_line_around_cursor('', 'aaa b{b}b ccc') input_keys('2W') - assert_byte_pointer_size('aaa b{b}b ') - assert_cursor(10) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b ', 'ccc') input_keys('2W') - assert_byte_pointer_size('aaa b{b}b cc') - assert_cursor(12) - assert_cursor_max(13) + assert_line_around_cursor('aaa b{b}b cc', 'c') input_keys('2B') - assert_byte_pointer_size('aaa ') - assert_cursor(4) - assert_cursor_max(13) + assert_line_around_cursor('aaa ', 'b{b}b ccc') input_keys('2B') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(13) + assert_line_around_cursor('', 'aaa b{b}b ccc') end def test_vi_end_big_word input_keys("aaa b{b}}}b ccc\C-[0") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(19) + assert_line_around_cursor('', 'aaa b{b}}}b ccc') input_keys('E') - assert_byte_pointer_size('aa') - assert_cursor(2) - assert_cursor_max(19) + assert_line_around_cursor('aa', 'a b{b}}}b ccc') input_keys('E') - assert_byte_pointer_size('aaa b{b}}}') - assert_cursor(12) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{b}}}', 'b ccc') input_keys('E') - assert_byte_pointer_size('aaa b{b}}}b cc') - assert_cursor(18) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{b}}}b cc', 'c') input_keys('E') - assert_byte_pointer_size('aaa b{b}}}b cc') - assert_cursor(18) - assert_cursor_max(19) + assert_line_around_cursor('aaa b{b}}}b cc', 'c') end def test_ed_quoted_insert input_keys("ab\C-v\C-acd") - assert_line("ab\C-acd") - assert_byte_pointer_size("ab\C-acd") - assert_cursor(6) - assert_cursor_max(6) + assert_line_around_cursor("ab\C-acd", '') end def test_ed_quoted_insert_with_vi_arg input_keys("ab\C-[3\C-v\C-aacd") - assert_line("a\C-a\C-a\C-abcd") - assert_byte_pointer_size("a\C-a\C-a\C-abcd") - assert_cursor(10) - assert_cursor_max(10) + assert_line_around_cursor("a\C-a\C-a\C-abcd", '') end def test_vi_replace_char input_keys("abcdef\C-[03l") - assert_line('abcdef') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(6) + assert_line_around_cursor('abc', 'def') input_keys('rz') - assert_line('abczef') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(6) + assert_line_around_cursor('abc', 'zef') input_keys('2rx') - assert_line('abcxxf') - assert_byte_pointer_size('abcxx') - assert_cursor(5) - assert_cursor_max(6) + assert_line_around_cursor('abcxx', 'f') end def test_vi_replace_char_with_mbchar input_keys("あいうえお\C-[0l") - assert_line('あいうえお') - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor('あ', 'いうえお') input_keys('rx') - assert_line('あxうえお') - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(9) + assert_line_around_cursor('あ', 'xうえお') input_keys('l2ry') - assert_line('あxyyお') - assert_byte_pointer_size('あxyy') - assert_cursor(5) - assert_cursor_max(7) + assert_line_around_cursor('あxyy', 'お') end def test_vi_next_char input_keys("abcdef\C-[0") - assert_line('abcdef') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'abcdef') input_keys('fz') - assert_line('abcdef') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'abcdef') input_keys('fe') - assert_line('abcdef') - assert_byte_pointer_size('abcd') - assert_cursor(4) - assert_cursor_max(6) + assert_line_around_cursor('abcd', 'ef') end def test_vi_to_next_char input_keys("abcdef\C-[0") - assert_line('abcdef') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'abcdef') input_keys('tz') - assert_line('abcdef') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'abcdef') input_keys('te') - assert_line('abcdef') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(6) + assert_line_around_cursor('abc', 'def') end def test_vi_prev_char input_keys("abcdef\C-[") - assert_line('abcdef') - assert_byte_pointer_size('abcde') - assert_cursor(5) - assert_cursor_max(6) + assert_line_around_cursor('abcde', 'f') input_keys('Fz') - assert_line('abcdef') - assert_byte_pointer_size('abcde') - assert_cursor(5) - assert_cursor_max(6) + assert_line_around_cursor('abcde', 'f') input_keys('Fa') - assert_line('abcdef') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', 'abcdef') end def test_vi_to_prev_char input_keys("abcdef\C-[") - assert_line('abcdef') - assert_byte_pointer_size('abcde') - assert_cursor(5) - assert_cursor_max(6) + assert_line_around_cursor('abcde', 'f') input_keys('Tz') - assert_line('abcdef') - assert_byte_pointer_size('abcde') - assert_cursor(5) - assert_cursor_max(6) + assert_line_around_cursor('abcde', 'f') input_keys('Ta') - assert_line('abcdef') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(6) + assert_line_around_cursor('a', 'bcdef') end def test_vi_delete_next_char input_keys("abc\C-[h") - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(3) - assert_line('abc') + assert_line_around_cursor('a', 'bc') input_keys('x') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(2) - assert_line('ac') + assert_line_around_cursor('a', 'c') input_keys('x') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(1) - assert_line('a') + assert_line_around_cursor('', 'a') input_keys('x') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('x') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_vi_delete_next_char_for_mbchar input_keys("あいう\C-[h") - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(6) - assert_line('あいう') + assert_line_around_cursor('あ', 'いう') input_keys('x') - assert_byte_pointer_size('あ') - assert_cursor(2) - assert_cursor_max(4) - assert_line('あう') + assert_line_around_cursor('あ', 'う') input_keys('x') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) - assert_line('あ') + assert_line_around_cursor('', 'あ') input_keys('x') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('x') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_vi_delete_next_char_for_mbchar_by_plural_code_points input_keys("か\u3099き\u3099く\u3099\C-[h") - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(6) - assert_line("か\u3099き\u3099く\u3099") + assert_line_around_cursor("か\u3099", "き\u3099く\u3099") input_keys('x') - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(4) - assert_line("か\u3099く\u3099") + assert_line_around_cursor("か\u3099", "く\u3099") input_keys('x') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(2) - assert_line("か\u3099") + assert_line_around_cursor('', "か\u3099") input_keys('x') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('x') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_vi_delete_prev_char input_keys('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') input_keys("\C-h") - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(1) - assert_line('a') + assert_line_around_cursor('a', '') end def test_vi_delete_prev_char_for_mbchar input_keys('かき') - assert_byte_pointer_size('かき') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('かき', '') input_keys("\C-h") - assert_byte_pointer_size('か') - assert_cursor(2) - assert_cursor_max(2) - assert_line('か') + assert_line_around_cursor('か', '') end def test_vi_delete_prev_char_for_mbchar_by_plural_code_points input_keys("か\u3099き\u3099") - assert_byte_pointer_size("か\u3099き\u3099") - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor("か\u3099き\u3099", '') input_keys("\C-h") - assert_byte_pointer_size("か\u3099") - assert_cursor(2) - assert_cursor_max(2) - assert_line("か\u3099") + assert_line_around_cursor("か\u3099", '') end def test_ed_delete_prev_char input_keys("abcdefg\C-[h") - assert_byte_pointer_size('abcde') - assert_cursor(5) - assert_cursor_max(7) - assert_line('abcdefg') + assert_line_around_cursor('abcde', 'fg') input_keys('X') - assert_byte_pointer_size('abcd') - assert_cursor(4) - assert_cursor_max(6) - assert_line('abcdfg') + assert_line_around_cursor('abcd', 'fg') input_keys('3X') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(3) - assert_line('afg') + assert_line_around_cursor('a', 'fg') input_keys('p') - assert_byte_pointer_size('abcd') - assert_cursor(4) - assert_cursor_max(6) - assert_line('afbcdg') + assert_line_around_cursor('afbc', 'dg') end def test_ed_delete_prev_word input_keys('abc def{bbb}ccc') - assert_byte_pointer_size('abc def{bbb}ccc') - assert_cursor(15) - assert_cursor_max(15) + assert_line_around_cursor('abc def{bbb}ccc', '') input_keys("\C-w") - assert_byte_pointer_size('abc def{bbb}') - assert_cursor(12) - assert_cursor_max(12) - assert_line('abc def{bbb}') + assert_line_around_cursor('abc def{bbb}', '') input_keys("\C-w") - assert_byte_pointer_size('abc def{') - assert_cursor(8) - assert_cursor_max(8) - assert_line('abc def{') + assert_line_around_cursor('abc def{', '') input_keys("\C-w") - assert_byte_pointer_size('abc ') - assert_cursor(4) - assert_cursor_max(4) - assert_line('abc ') + assert_line_around_cursor('abc ', '') input_keys("\C-w") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_ed_delete_prev_word_for_mbchar input_keys('あいう かきく{さしす}たちつ') - assert_byte_pointer_size('あいう かきく{さしす}たちつ') - assert_cursor(27) - assert_cursor_max(27) + assert_line_around_cursor('あいう かきく{さしす}たちつ', '') input_keys("\C-w") - assert_byte_pointer_size('あいう かきく{さしす}') - assert_cursor(21) - assert_cursor_max(21) - assert_line('あいう かきく{さしす}') + assert_line_around_cursor('あいう かきく{さしす}', '') input_keys("\C-w") - assert_byte_pointer_size('あいう かきく{') - assert_cursor(14) - assert_cursor_max(14) - assert_line('あいう かきく{') + assert_line_around_cursor('あいう かきく{', '') input_keys("\C-w") - assert_byte_pointer_size('あいう ') - assert_cursor(7) - assert_cursor_max(7) - assert_line('あいう ') + assert_line_around_cursor('あいう ', '') input_keys("\C-w") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_ed_delete_prev_word_for_mbchar_by_plural_code_points input_keys("あいう か\u3099き\u3099く\u3099{さしす}たちつ") - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{さしす}たちつ") - assert_cursor(27) - assert_cursor_max(27) + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{さしす}たちつ", '') input_keys("\C-w") - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{さしす}") - assert_cursor(21) - assert_cursor_max(21) - assert_line("あいう か\u3099き\u3099く\u3099{さしす}") + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{さしす}", '') input_keys("\C-w") - assert_byte_pointer_size("あいう か\u3099き\u3099く\u3099{") - assert_cursor(14) - assert_cursor_max(14) - assert_line("あいう か\u3099き\u3099く\u3099{") + assert_line_around_cursor("あいう か\u3099き\u3099く\u3099{", '') input_keys("\C-w") - assert_byte_pointer_size('あいう ') - assert_cursor(7) - assert_cursor_max(7) - assert_line('あいう ') + assert_line_around_cursor('あいう ', '') input_keys("\C-w") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_ed_newline_with_cr input_keys('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') refute(@line_editor.finished?) input_keys("\C-m") - assert_line('ab') + assert_line_around_cursor('ab', '') assert(@line_editor.finished?) end def test_ed_newline_with_lf input_keys('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') refute(@line_editor.finished?) input_keys("\C-j") - assert_line('ab') + assert_line_around_cursor('ab', '') assert(@line_editor.finished?) end def test_vi_list_or_eof input_keys("\C-d") # quit from inputing - assert_line(nil) + assert_nil(@line_editor.line) assert(@line_editor.finished?) end def test_vi_list_or_eof_with_non_empty_line input_keys('ab') - assert_byte_pointer_size('ab') - assert_cursor(2) - assert_cursor_max(2) + assert_line_around_cursor('ab', '') refute(@line_editor.finished?) input_keys("\C-d") - assert_line('ab') + assert_line_around_cursor('ab', '') assert(@line_editor.finished?) end @@ -982,40 +545,19 @@ class Reline::KeyActor::ViInsert::Test < Reline::TestCase } } input_keys('foo') - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') input_keys("\C-n") - assert_byte_pointer_size('foo_bar') - assert_cursor(7) - assert_cursor_max(7) - assert_line('foo_bar') + assert_line_around_cursor('foo_bar', '') input_keys("\C-n") - assert_byte_pointer_size('foo_bar_baz') - assert_cursor(11) - assert_cursor_max(11) - assert_line('foo_bar_baz') + assert_line_around_cursor('foo_bar_baz', '') input_keys("\C-n") - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') input_keys("\C-n") - assert_byte_pointer_size('foo_bar') - assert_cursor(7) - assert_cursor_max(7) - assert_line('foo_bar') + assert_line_around_cursor('foo_bar', '') input_keys("_\C-n") - assert_byte_pointer_size('foo_bar_baz') - assert_cursor(11) - assert_cursor_max(11) - assert_line('foo_bar_baz') + assert_line_around_cursor('foo_bar_baz', '') input_keys("\C-n") - assert_byte_pointer_size('foo_bar_') - assert_cursor(8) - assert_cursor_max(8) - assert_line('foo_bar_') + assert_line_around_cursor('foo_bar_', '') end def test_completion_journey_reverse @@ -1028,40 +570,19 @@ class Reline::KeyActor::ViInsert::Test < Reline::TestCase } } input_keys('foo') - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') input_keys("\C-p") - assert_byte_pointer_size('foo_bar_baz') - assert_cursor(11) - assert_cursor_max(11) - assert_line('foo_bar_baz') + assert_line_around_cursor('foo_bar_baz', '') input_keys("\C-p") - assert_byte_pointer_size('foo_bar') - assert_cursor(7) - assert_cursor_max(7) - assert_line('foo_bar') + assert_line_around_cursor('foo_bar', '') input_keys("\C-p") - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') input_keys("\C-p") - assert_byte_pointer_size('foo_bar_baz') - assert_cursor(11) - assert_cursor_max(11) - assert_line('foo_bar_baz') + assert_line_around_cursor('foo_bar_baz', '') input_keys("\C-h\C-p") - assert_byte_pointer_size('foo_bar_baz') - assert_cursor(11) - assert_cursor_max(11) - assert_line('foo_bar_baz') + assert_line_around_cursor('foo_bar_baz', '') input_keys("\C-p") - assert_byte_pointer_size('foo_bar_ba') - assert_cursor(10) - assert_cursor_max(10) - assert_line('foo_bar_ba') + assert_line_around_cursor('foo_bar_ba', '') end def test_completion_journey_in_middle_of_line @@ -1074,42 +595,21 @@ class Reline::KeyActor::ViInsert::Test < Reline::TestCase } } input_keys('abcde fo ABCDE') - assert_line('abcde fo ABCDE') + assert_line_around_cursor('abcde fo ABCDE', '') input_keys("\C-[" + 'h' * 5 + "i\C-n") - assert_byte_pointer_size('abcde foo_bar') - assert_cursor(13) - assert_cursor_max(19) - assert_line('abcde foo_bar ABCDE') + assert_line_around_cursor('abcde foo_bar', ' ABCDE') input_keys("\C-n") - assert_byte_pointer_size('abcde foo_bar_baz') - assert_cursor(17) - assert_cursor_max(23) - assert_line('abcde foo_bar_baz ABCDE') + assert_line_around_cursor('abcde foo_bar_baz', ' ABCDE') input_keys("\C-n") - assert_byte_pointer_size('abcde fo') - assert_cursor(8) - assert_cursor_max(14) - assert_line('abcde fo ABCDE') + assert_line_around_cursor('abcde fo', ' ABCDE') input_keys("\C-n") - assert_byte_pointer_size('abcde foo_bar') - assert_cursor(13) - assert_cursor_max(19) - assert_line('abcde foo_bar ABCDE') + assert_line_around_cursor('abcde foo_bar', ' ABCDE') input_keys("_\C-n") - assert_byte_pointer_size('abcde foo_bar_baz') - assert_cursor(17) - assert_cursor_max(23) - assert_line('abcde foo_bar_baz ABCDE') + assert_line_around_cursor('abcde foo_bar_baz', ' ABCDE') input_keys("\C-n") - assert_byte_pointer_size('abcde foo_bar_') - assert_cursor(14) - assert_cursor_max(20) - assert_line('abcde foo_bar_ ABCDE') + assert_line_around_cursor('abcde foo_bar_', ' ABCDE') input_keys("\C-n") - assert_byte_pointer_size('abcde foo_bar_baz') - assert_cursor(17) - assert_cursor_max(23) - assert_line('abcde foo_bar_baz ABCDE') + assert_line_around_cursor('abcde foo_bar_baz', ' ABCDE') end def test_completion @@ -1122,15 +622,55 @@ class Reline::KeyActor::ViInsert::Test < Reline::TestCase } } input_keys('foo') - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') input_keys("\C-i") - assert_byte_pointer_size('foo_bar') - assert_cursor(7) - assert_cursor_max(7) - assert_line('foo_bar') + assert_line_around_cursor('foo_bar', '') + end + + def test_autocompletion_with_upward_navigation + @config.autocompletion = true + @line_editor.completion_proc = proc { |word| + %w{ + Readline + Regexp + RegexpError + }.map { |i| + i.encode(@encoding) + } + } + input_keys('Re') + assert_line_around_cursor('Re', '') + input_keys("\C-i", false) + assert_line_around_cursor('Readline', '') + input_keys("\C-i", false) + assert_line_around_cursor('Regexp', '') + @line_editor.input_key(Reline::Key.new(:completion_journey_up, :completion_journey_up, false)) + assert_line_around_cursor('Readline', '') + ensure + @config.autocompletion = false + end + + def test_autocompletion_with_upward_navigation_and_menu_complete_backward + @config.autocompletion = true + @line_editor.completion_proc = proc { |word| + %w{ + Readline + Regexp + RegexpError + }.map { |i| + i.encode(@encoding) + } + } + input_keys('Re') + assert_line_around_cursor('Re', '') + input_keys("\C-i", false) + assert_line_around_cursor('Readline', '') + input_keys("\C-i", false) + assert_line_around_cursor('Regexp', '') + @line_editor.input_key(Reline::Key.new(:menu_complete_backward, :menu_complete_backward, false)) + assert_line_around_cursor('Readline', '') + ensure + @config.autocompletion = false end def test_completion_with_disable_completion @@ -1144,315 +684,224 @@ class Reline::KeyActor::ViInsert::Test < Reline::TestCase } } input_keys('foo') - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') input_keys("\C-i") - assert_byte_pointer_size('foo') - assert_cursor(3) - assert_cursor_max(3) - assert_line('foo') + assert_line_around_cursor('foo', '') end def test_vi_first_print input_keys("abcde\C-[^") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) + assert_line_around_cursor('', 'abcde') input_keys("0\C-ki") input_keys(" abcde\C-[^") - assert_byte_pointer_size(' ') - assert_cursor(1) - assert_cursor_max(6) + assert_line_around_cursor(' ', 'abcde') input_keys("0\C-ki") input_keys(" abcde ABCDE \C-[^") - assert_byte_pointer_size(' ') - assert_cursor(3) - assert_cursor_max(17) + assert_line_around_cursor(' ', 'abcde ABCDE ') end def test_ed_move_to_beg input_keys("abcde\C-[0") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(5) + assert_line_around_cursor('', 'abcde') input_keys("0\C-ki") input_keys(" abcde\C-[0") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(6) + assert_line_around_cursor('', ' abcde') input_keys("0\C-ki") input_keys(" abcde ABCDE \C-[0") - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(17) + assert_line_around_cursor('', ' abcde ABCDE ') + end + + def test_vi_to_column + input_keys("a一二三\C-[0") + input_keys('1|') + assert_line_around_cursor('', 'a一二三') + input_keys('2|') + assert_line_around_cursor('a', '一二三') + input_keys('3|') + assert_line_around_cursor('a', '一二三') + input_keys('4|') + assert_line_around_cursor('a一', '二三') + input_keys('9|') + assert_line_around_cursor('a一二', '三') end def test_vi_delete_meta input_keys("aaa bbb ccc ddd eee\C-[02w") - assert_byte_pointer_size('aaa bbb ') - assert_cursor(8) - assert_cursor_max(19) - assert_line('aaa bbb ccc ddd eee') + assert_line_around_cursor('aaa bbb ', 'ccc ddd eee') input_keys('dw') - assert_byte_pointer_size('aaa bbb ') - assert_cursor(8) - assert_cursor_max(15) - assert_line('aaa bbb ddd eee') + assert_line_around_cursor('aaa bbb ', 'ddd eee') input_keys('db') - assert_byte_pointer_size('aaa ') - assert_cursor(4) - assert_cursor_max(11) - assert_line('aaa ddd eee') + assert_line_around_cursor('aaa ', 'ddd eee') end def test_vi_delete_meta_with_vi_next_word_at_eol input_keys("foo bar\C-[0w") - assert_byte_pointer_size('foo ') - assert_cursor(4) - assert_cursor_max(7) - assert_line('foo bar') + assert_line_around_cursor('foo ', 'bar') input_keys('w') - assert_byte_pointer_size('foo ba') - assert_cursor(6) - assert_cursor_max(7) - assert_line('foo bar') + assert_line_around_cursor('foo ba', 'r') input_keys('0dw') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) - assert_line('bar') + assert_line_around_cursor('', 'bar') input_keys('dw') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') end def test_vi_delete_meta_with_vi_next_char input_keys("aaa bbb ccc ___ ddd\C-[02w") - assert_byte_pointer_size('aaa bbb ') - assert_cursor(8) - assert_cursor_max(19) - assert_line('aaa bbb ccc ___ ddd') + assert_line_around_cursor('aaa bbb ', 'ccc ___ ddd') input_keys('df_') - assert_byte_pointer_size('aaa bbb ') - assert_cursor(8) - assert_cursor_max(14) - assert_line('aaa bbb __ ddd') + assert_line_around_cursor('aaa bbb ', '__ ddd') end def test_vi_delete_meta_with_arg - input_keys("aaa bbb ccc\C-[02w") - assert_byte_pointer_size('aaa bbb ') - assert_cursor(8) - assert_cursor_max(11) - assert_line('aaa bbb ccc') + input_keys("aaa bbb ccc ddd\C-[03w") + assert_line_around_cursor('aaa bbb ccc ', 'ddd') input_keys('2dl') - assert_byte_pointer_size('aaa bbb ') - assert_cursor(8) - assert_cursor_max(9) - assert_line('aaa bbb c') + assert_line_around_cursor('aaa bbb ccc ', 'd') + input_keys('d2h') + assert_line_around_cursor('aaa bbb cc', 'd') + input_keys('2d3h') + assert_line_around_cursor('aaa ', 'd') + input_keys('dd') + assert_line_around_cursor('', '') end def test_vi_change_meta input_keys("aaa bbb ccc ddd eee\C-[02w") - assert_byte_pointer_size('aaa bbb ') - assert_cursor(8) - assert_cursor_max(19) - assert_line('aaa bbb ccc ddd eee') + assert_line_around_cursor('aaa bbb ', 'ccc ddd eee') input_keys('cwaiueo') - assert_byte_pointer_size('aaa bbb aiueo') - assert_cursor(13) - assert_cursor_max(21) - assert_line('aaa bbb aiueo ddd eee') + assert_line_around_cursor('aaa bbb aiueo', ' ddd eee') input_keys("\C-[") - assert_byte_pointer_size('aaa bbb aiue') - assert_cursor(12) - assert_cursor_max(21) - assert_line('aaa bbb aiueo ddd eee') + assert_line_around_cursor('aaa bbb aiue', 'o ddd eee') input_keys('cb') - assert_byte_pointer_size('aaa bbb ') - assert_cursor(8) - assert_cursor_max(17) - assert_line('aaa bbb o ddd eee') + assert_line_around_cursor('aaa bbb ', 'o ddd eee') end def test_vi_change_meta_with_vi_next_word input_keys("foo bar baz\C-[0w") - assert_byte_pointer_size('foo ') - assert_cursor(5) - assert_cursor_max(13) - assert_line('foo bar baz') + assert_line_around_cursor('foo ', 'bar baz') input_keys('cwhoge') - assert_byte_pointer_size('foo hoge') - assert_cursor(9) - assert_cursor_max(14) - assert_line('foo hoge baz') + assert_line_around_cursor('foo hoge', ' baz') input_keys("\C-[") - assert_byte_pointer_size('foo hog') - assert_cursor(8) - assert_cursor_max(14) - assert_line('foo hoge baz') + assert_line_around_cursor('foo hog', 'e baz') + end + + def test_vi_waiting_operator_with_waiting_proc + input_keys("foo foo foo foo foo\C-[0") + input_keys('2d3fo') + assert_line_around_cursor('', ' foo foo') + input_keys('fo') + assert_line_around_cursor(' f', 'oo foo') + end + + def test_vi_waiting_operator_cancel + input_keys("aaa bbb ccc\C-[02w") + assert_line_around_cursor('aaa bbb ', 'ccc') + # dc dy should cancel delete_meta + input_keys('dch') + input_keys('dyh') + # cd cy should cancel change_meta + input_keys('cdh') + input_keys('cyh') + # yd yc should cancel yank_meta + # P should not paste yanked text because yank_meta is canceled + input_keys('ydhP') + input_keys('ychP') + assert_line_around_cursor('aa', 'a bbb ccc') + end + + def test_cancel_waiting_with_symbol_key + input_keys("aaa bbb lll\C-[0") + assert_line_around_cursor('', 'aaa bbb lll') + # ed_next_char should move cursor right and cancel vi_next_char + input_keys('f') + input_key_by_symbol(:ed_next_char) + input_keys('l') + assert_line_around_cursor('aa', 'a bbb lll') + # ed_next_char should move cursor right and cancel delete_meta + input_keys('d') + input_key_by_symbol(:ed_next_char) + input_keys('l') + assert_line_around_cursor('aaa ', 'bbb lll') end def test_unimplemented_vi_command_should_be_no_op input_keys("abc\C-[h") - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(3) - assert_line('abc') + assert_line_around_cursor('a', 'bc') input_keys('@') - assert_byte_pointer_size('a') - assert_cursor(1) - assert_cursor_max(3) - assert_line('abc') + assert_line_around_cursor('a', 'bc') end def test_vi_yank - input_keys("foo bar\C-[0") - assert_line('foo bar') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(7) + input_keys("foo bar\C-[2h") + assert_line_around_cursor('foo ', 'bar') input_keys('y3l') - assert_line('foo bar') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(7) + assert_line_around_cursor('foo ', 'bar') input_keys('P') - assert_line('foofoo bar') - assert_byte_pointer_size('fo') - assert_cursor(2) - assert_cursor_max(10) + assert_line_around_cursor('foo ba', 'rbar') + input_keys('3h3yhP') + assert_line_around_cursor('foofo', 'o barbar') + input_keys('yyP') + assert_line_around_cursor('foofofoofoo barba', 'ro barbar') end def test_vi_end_word_with_operator input_keys("foo bar\C-[0") - assert_line('foo bar') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(7) + assert_line_around_cursor('', 'foo bar') input_keys('de') - assert_line(' bar') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(4) + assert_line_around_cursor('', ' bar') input_keys('de') - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys('de') - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') end def test_vi_end_big_word_with_operator input_keys("aaa b{b}}}b\C-[0") - assert_line('aaa b{b}}}b') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(13) + assert_line_around_cursor('', 'aaa b{b}}}b') input_keys('dE') - assert_line(' b{b}}}b') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(10) + assert_line_around_cursor('', ' b{b}}}b') input_keys('dE') - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') input_keys('dE') - assert_line('') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) + assert_line_around_cursor('', '') end def test_vi_next_char_with_operator input_keys("foo bar\C-[0") - assert_line('foo bar') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(7) + assert_line_around_cursor('', 'foo bar') input_keys('df ') - assert_line('bar') - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(3) - end - - def test_pasting - start_pasting - input_keys('ab') - finish_pasting - input_keys('c') - assert_line('abc') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(3) - end - - def test_pasting_fullwidth - start_pasting - input_keys('あ') - finish_pasting - input_keys('い') - assert_line('あい') - assert_byte_pointer_size('あい') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('', 'bar') end def test_ed_delete_next_char_at_eol input_keys('"あ"') - assert_line('"あ"') - assert_byte_pointer_size('"あ"') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('"あ"', '') input_keys("\C-[") - assert_line('"あ"') - assert_byte_pointer_size('"あ') - assert_cursor(3) - assert_cursor_max(4) + assert_line_around_cursor('"あ', '"') input_keys('xa"') - assert_line('"あ"') - assert_byte_pointer_size('"あ"') - assert_cursor(4) - assert_cursor_max(4) + assert_line_around_cursor('"あ"', '') end def test_vi_kill_line_prev input_keys("\C-u", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('abc') - assert_byte_pointer_size('abc') - assert_cursor(3) - assert_cursor_max(3) + assert_line_around_cursor('abc', '') input_keys("\C-u", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(0) - assert_line('') + assert_line_around_cursor('', '') input_keys('abc') input_keys("\C-[\C-u", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(1) - assert_line('c') + assert_line_around_cursor('', 'c') input_keys("\C-u", false) - assert_byte_pointer_size('') - assert_cursor(0) - assert_cursor_max(1) - assert_line('c') + assert_line_around_cursor('', 'c') + end + + def test_vi_change_to_eol + input_keys("abcdef\C-[2hC") + assert_line_around_cursor('abc', '') + input_keys("\C-[0C") + assert_line_around_cursor('', '') + assert_instance_of(Reline::KeyActor::ViInsert, @config.editing_mode) end def test_vi_motion_operators @@ -1462,4 +911,9 @@ class Reline::KeyActor::ViInsert::Test < Reline::TestCase input_keys("test = { foo: bar }\C-[BBBldt}b") end end + + def test_emacs_editing_mode + @line_editor.__send__(:emacs_editing_mode, nil) + assert(@config.editing_mode_is?(:emacs)) + end end diff --git a/test/reline/test_line_editor.rb b/test/reline/test_line_editor.rb index 8399e76e92..7a38ecd596 100644 --- a/test/reline/test_line_editor.rb +++ b/test/reline/test_line_editor.rb @@ -1,13 +1,185 @@ require_relative 'helper' require 'reline/line_editor' +require 'stringio' -class Reline::LineEditor::Test < Reline::TestCase - def test_range_subtract - dummy_config = nil - editor = Reline::LineEditor.new(dummy_config, 'ascii-8bit') - base_ranges = [3...5, 4...10, 6...8, 12...15, 15...20] - subtract_ranges = [5...7, 8...9, 11...13, 17...18, 18...19] - expected_result = [3...5, 7...8, 9...10, 13...17, 19...20] - assert_equal expected_result, editor.send(:range_subtract, base_ranges, subtract_ranges) +class Reline::LineEditor + class RenderLineDifferentialTest < Reline::TestCase + module TestIO + RESET_COLOR = "\e[0m" + + def self.move_cursor_column(col) + @output << "[COL_#{col}]" + end + + def self.erase_after_cursor + @output << '[ERASE]' + end + end + + def setup + verbose, $VERBOSE = $VERBOSE, nil + @line_editor = Reline::LineEditor.new(nil, Encoding::UTF_8) + @original_iogate = Reline::IOGate + @output = StringIO.new + @line_editor.instance_variable_set(:@screen_size, [24, 80]) + @line_editor.instance_variable_set(:@output, @output) + Reline.send(:remove_const, :IOGate) + Reline.const_set(:IOGate, TestIO) + Reline::IOGate.instance_variable_set(:@output, @output) + ensure + $VERBOSE = verbose + end + + def assert_output(expected) + @output.reopen(+'') + yield + actual = @output.string + assert_equal(expected, actual.gsub("\e[0m", '')) + end + + def teardown + Reline.send(:remove_const, :IOGate) + Reline.const_set(:IOGate, @original_iogate) + end + + def test_line_increase_decrease + assert_output '[COL_0]bb' do + @line_editor.render_line_differential([[0, 1, 'a']], [[0, 2, 'bb']]) + end + + assert_output '[COL_0]b[COL_1][ERASE]' do + @line_editor.render_line_differential([[0, 2, 'aa']], [[0, 1, 'b']]) + end + end + + def test_dialog_appear_disappear + assert_output '[COL_3]dialog' do + @line_editor.render_line_differential([[0, 1, 'a']], [[0, 1, 'a'], [3, 6, 'dialog']]) + end + + assert_output '[COL_3]dialog' do + @line_editor.render_line_differential([[0, 10, 'a' * 10]], [[0, 10, 'a' * 10], [3, 6, 'dialog']]) + end + + assert_output '[COL_1][ERASE]' do + @line_editor.render_line_differential([[0, 1, 'a'], [3, 6, 'dialog']], [[0, 1, 'a']]) + end + + assert_output '[COL_3]aaaaaa' do + @line_editor.render_line_differential([[0, 10, 'a' * 10], [3, 6, 'dialog']], [[0, 10, 'a' * 10]]) + end + end + + def test_dialog_change + assert_output '[COL_3]DIALOG' do + @line_editor.render_line_differential([[0, 2, 'a'], [3, 6, 'dialog']], [[0, 2, 'a'], [3, 6, 'DIALOG']]) + end + + assert_output '[COL_3]DIALOG' do + @line_editor.render_line_differential([[0, 10, 'a' * 10], [3, 6, 'dialog']], [[0, 10, 'a' * 10], [3, 6, 'DIALOG']]) + end + end + + def test_update_under_dialog + assert_output '[COL_0]b[COL_1] ' do + @line_editor.render_line_differential([[0, 2, 'aa'], [4, 6, 'dialog']], [[0, 1, 'b'], [4, 6, 'dialog']]) + end + + assert_output '[COL_0]bbb[COL_9]b' do + @line_editor.render_line_differential([[0, 10, 'a' * 10], [3, 6, 'dialog']], [[0, 10, 'b' * 10], [3, 6, 'dialog']]) + end + + assert_output '[COL_0]b[COL_1] [COL_9][ERASE]' do + @line_editor.render_line_differential([[0, 10, 'a' * 10], [3, 6, 'dialog']], [[0, 1, 'b'], [3, 6, 'dialog']]) + end + end + + def test_dialog_move + assert_output '[COL_3]dialog[COL_9][ERASE]' do + @line_editor.render_line_differential([[0, 1, 'a'], [4, 6, 'dialog']], [[0, 1, 'a'], [3, 6, 'dialog']]) + end + + assert_output '[COL_4] [COL_5]dialog' do + @line_editor.render_line_differential([[0, 1, 'a'], [4, 6, 'dialog']], [[0, 1, 'a'], [5, 6, 'dialog']]) + end + + assert_output '[COL_2]dialog[COL_8]a' do + @line_editor.render_line_differential([[0, 10, 'a' * 10], [3, 6, 'dialog']], [[0, 10, 'a' * 10], [2, 6, 'dialog']]) + end + + assert_output '[COL_2]a[COL_3]dialog' do + @line_editor.render_line_differential([[0, 10, 'a' * 10], [2, 6, 'dialog']], [[0, 10, 'a' * 10], [3, 6, 'dialog']]) + end + end + + def test_multibyte + base = [0, 12, '一二三一二三'] + left = [0, 3, 'LLL'] + right = [9, 3, 'RRR'] + front = [3, 6, 'FFFFFF'] + # 一 FFFFFF 三 + # 一二三一二三 + assert_output '[COL_2]二三一二' do + @line_editor.render_line_differential([base, front], [base, nil]) + end + + # LLLFFFFFF 三 + # LLL 三一二三 + assert_output '[COL_3] 三一二' do + @line_editor.render_line_differential([base, left, front], [base, left, nil]) + end + + # 一 FFFFFFRRR + # 一二三一 RRR + assert_output '[COL_2]二三一 ' do + @line_editor.render_line_differential([base, right, front], [base, right, nil]) + end + + # LLLFFFFFFRRR + # LLL 三一 RRR + assert_output '[COL_3] 三一 ' do + @line_editor.render_line_differential([base, left, right, front], [base, left, right, nil]) + end + end + + def test_complicated + state_a = [nil, [19, 7, 'bbbbbbb'], [15, 8, 'cccccccc'], [10, 5, 'ddddd'], [18, 4, 'eeee'], [1, 3, 'fff'], [17, 2, 'gg'], [7, 1, 'h']] + state_b = [[5, 9, 'aaaaaaaaa'], nil, [15, 8, 'cccccccc'], nil, [18, 4, 'EEEE'], [25, 4, 'ffff'], [17, 2, 'gg'], [2, 2, 'hh']] + # state_a: " fff h dddddccggeeecbbb" + # state_b: " hh aaaaaaaaa ccggEEEc ffff" + + assert_output '[COL_1] [COL_2]hh[COL_5]aaaaaaaaa[COL_14] [COL_19]EEE[COL_23] [COL_25]ffff' do + @line_editor.render_line_differential(state_a, state_b) + end + + assert_output '[COL_1]fff[COL_5] [COL_7]h[COL_8] [COL_10]ddddd[COL_19]eee[COL_23]bbb[COL_26][ERASE]' do + @line_editor.render_line_differential(state_b, state_a) + end + end + end + + def test_menu_info_format + list = %w[aa b c d e f g hhh i j k] + col3 = [ + 'aa e i', + 'b f j', + 'c g k', + 'd hhh' + ] + col2 = [ + 'aa g', + 'b hhh', + 'c i', + 'd j', + 'e k', + 'f' + ] + assert_equal(col3, Reline::LineEditor::MenuInfo.new(list).lines(19)) + assert_equal(col3, Reline::LineEditor::MenuInfo.new(list).lines(15)) + assert_equal(col2, Reline::LineEditor::MenuInfo.new(list).lines(14)) + assert_equal(col2, Reline::LineEditor::MenuInfo.new(list).lines(10)) + assert_equal(list, Reline::LineEditor::MenuInfo.new(list).lines(9)) + assert_equal(list, Reline::LineEditor::MenuInfo.new(list).lines(0)) + assert_equal([], Reline::LineEditor::MenuInfo.new([]).lines(10)) end end diff --git a/test/reline/test_macro.rb b/test/reline/test_macro.rb index 3096930830..04aa6474b4 100644 --- a/test/reline/test_macro.rb +++ b/test/reline/test_macro.rb @@ -6,7 +6,6 @@ class Reline::MacroTest < Reline::TestCase @config = Reline::Config.new @encoding = Reline.core.encoding @line_editor = Reline::LineEditor.new(@config, @encoding) - @line_editor.instance_variable_set(:@screen_size, [24, 80]) @output = @line_editor.output = File.open(IO::NULL, "w") end diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index 40c880c11f..a20a5c9f44 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -378,10 +378,28 @@ class Reline::Test < Reline::TestCase assert_equal("Reline::GeneralIO", out.chomp) end + def test_require_reline_should_not_trigger_winsize + pend if win? + lib = File.expand_path("../../lib", __dir__) + code = <<~RUBY + require "io/console" + def STDIN.tty?; true; end + def STDOUT.tty?; true; end + def STDIN.winsize; raise; end + require("reline") && p(Reline.core.io_gate) + RUBY + out = IO.popen([{}, Reline.test_rubybin, "-I#{lib}", "-e", code], &:read) + assert_equal("Reline::ANSI", out.chomp) + end + + def win? + /mswin|mingw/.match?(RUBY_PLATFORM) + end + def get_reline_encoding if encoding = Reline.core.encoding encoding - elsif RUBY_PLATFORM =~ /mswin|mingw/ + elsif win? Encoding::UTF_8 else Encoding::default_external diff --git a/test/reline/test_string_processing.rb b/test/reline/test_string_processing.rb index 2e5d27dc4f..c9b9e38643 100644 --- a/test/reline/test_string_processing.rb +++ b/test/reline/test_string_processing.rb @@ -30,10 +30,7 @@ class Reline::LineEditor::StringProcessingTest < Reline::TestCase @line_editor.instance_variable_set(:@is_multiline, true) @line_editor.instance_variable_set(:@buffer_of_lines, buf) - @line_editor.instance_variable_set(:@line, buf[1]) @line_editor.instance_variable_set(:@byte_pointer, 3) - @line_editor.instance_variable_set(:@cursor, 3) - @line_editor.instance_variable_set(:@cursor_max, 11) @line_editor.instance_variable_set(:@line_index, 1) @line_editor.instance_variable_set(:@completion_proc, proc { |target| assert_equal('p', target) @@ -42,10 +39,7 @@ class Reline::LineEditor::StringProcessingTest < Reline::TestCase @line_editor.instance_variable_set(:@is_multiline, true) @line_editor.instance_variable_set(:@buffer_of_lines, buf) - @line_editor.instance_variable_set(:@line, buf[1]) @line_editor.instance_variable_set(:@byte_pointer, 6) - @line_editor.instance_variable_set(:@cursor, 6) - @line_editor.instance_variable_set(:@cursor_max, 11) @line_editor.instance_variable_set(:@line_index, 1) @line_editor.instance_variable_set(:@completion_proc, proc { |target, pre, post| assert_equal('puts', target) @@ -54,10 +48,7 @@ class Reline::LineEditor::StringProcessingTest < Reline::TestCase }) @line_editor.__send__(:call_completion_proc) - @line_editor.instance_variable_set(:@line, buf[0]) @line_editor.instance_variable_set(:@byte_pointer, 6) - @line_editor.instance_variable_set(:@cursor, 6) - @line_editor.instance_variable_set(:@cursor_max, 8) @line_editor.instance_variable_set(:@line_index, 0) @line_editor.instance_variable_set(:@completion_proc, proc { |target, pre, post| assert_equal('ho', target) @@ -66,10 +57,7 @@ class Reline::LineEditor::StringProcessingTest < Reline::TestCase }) @line_editor.__send__(:call_completion_proc) - @line_editor.instance_variable_set(:@line, buf[2]) @line_editor.instance_variable_set(:@byte_pointer, 1) - @line_editor.instance_variable_set(:@cursor, 1) - @line_editor.instance_variable_set(:@cursor_max, 3) @line_editor.instance_variable_set(:@line_index, 2) @line_editor.instance_variable_set(:@completion_proc, proc { |target, pre, post| assert_equal('e', target) diff --git a/test/reline/test_terminfo.rb b/test/reline/test_terminfo.rb index dda9b32495..4e59c54838 100644 --- a/test/reline/test_terminfo.rb +++ b/test/reline/test_terminfo.rb @@ -58,4 +58,4 @@ class Reline::Terminfo::Test < Reline::TestCase assert_raise(Reline::Terminfo::TerminfoError) { Reline::Terminfo.tigetnum('unknown') } assert_raise(Reline::Terminfo::TerminfoError) { Reline::Terminfo.tigetnum(nil) } end -end if Reline::Terminfo.enabled? +end if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? diff --git a/test/reline/test_unicode.rb b/test/reline/test_unicode.rb index 834f7114c4..deba4d4681 100644 --- a/test/reline/test_unicode.rb +++ b/test/reline/test_unicode.rb @@ -38,11 +38,16 @@ class Reline::Unicode::Test < Reline::TestCase assert_equal [["ab\e]0;1\ac", nil, "\e]0;1\ad"], 2], Reline::Unicode.split_by_width("ab\e]0;1\acd", 3) end + def test_split_by_width_csi_reset_sgr_optimization + assert_equal [["\e[1ma\e[mb\e[2mc", nil, "\e[2md\e[0me\e[3mf", nil, "\e[3mg"], 3], Reline::Unicode.split_by_width("\e[1ma\e[mb\e[2mcd\e[0me\e[3mfg", 3) + assert_equal [["\e[1ma\1\e[mzero\e[0m\2\e[2mb", nil, "\e[1m\e[2mc"], 2], Reline::Unicode.split_by_width("\e[1ma\1\e[mzero\e[0m\2\e[2mbc", 2) + end + def test_take_range assert_equal 'cdef', Reline::Unicode.take_range('abcdefghi', 2, 4) assert_equal 'あde', Reline::Unicode.take_range('abあdef', 2, 4) - assert_equal 'zerocdef', Reline::Unicode.take_range("ab\1zero\2cdef", 2, 4) - assert_equal 'bzerocde', Reline::Unicode.take_range("ab\1zero\2cdef", 1, 4) + assert_equal "\1zero\2cdef", Reline::Unicode.take_range("ab\1zero\2cdef", 2, 4) + assert_equal "b\1zero\2cde", Reline::Unicode.take_range("ab\1zero\2cdef", 1, 4) assert_equal "\e[31mcd\e[42mef", Reline::Unicode.take_range("\e[31mabcd\e[42mefg", 2, 4) assert_equal "\e]0;1\acd", Reline::Unicode.take_range("ab\e]0;1\acd", 2, 3) assert_equal 'いう', Reline::Unicode.take_range('あいうえお', 2, 4) @@ -62,4 +67,26 @@ class Reline::Unicode::Test < Reline::TestCase assert_equal 10, Reline::Unicode.calculate_width('あいうえお') assert_equal 10, Reline::Unicode.calculate_width('あいうえお', true) end + + def test_take_mbchar_range + assert_equal ['cdef', 2, 4], Reline::Unicode.take_mbchar_range('abcdefghi', 2, 4) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_mbchar_range('abcdefghi', 2, 4, padding: true) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_mbchar_range('abcdefghi', 2, 4, cover_begin: true) + assert_equal ['cdef', 2, 4], Reline::Unicode.take_mbchar_range('abcdefghi', 2, 4, cover_end: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 2, 4) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 2, 4, padding: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 2, 4, cover_begin: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 2, 4, cover_end: true) + assert_equal ['う', 4, 2], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4) + assert_equal [' う ', 3, 4], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, padding: true) + assert_equal ['いう', 2, 4], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, cover_begin: true) + assert_equal ['うえ', 4, 4], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, cover_end: true) + assert_equal ['いう ', 2, 5], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, cover_begin: true, padding: true) + assert_equal [' うえ', 3, 5], Reline::Unicode.take_mbchar_range('あいうえお', 3, 4, cover_end: true, padding: true) + assert_equal [' うえお ', 3, 10], Reline::Unicode.take_mbchar_range('あいうえお', 3, 10, padding: true) + assert_equal [" \e[41mうえお\e[0m ", 3, 10], Reline::Unicode.take_mbchar_range("あい\e[41mうえお", 3, 10, padding: true) + assert_equal ["\e[41m \e[42mい\e[43m ", 1, 4], Reline::Unicode.take_mbchar_range("\e[41mあ\e[42mい\e[43mう", 1, 4, padding: true) + assert_equal ["\e[31mc\1ABC\2d\e[0mef", 2, 4], Reline::Unicode.take_mbchar_range("\e[31mabc\1ABC\2d\e[0mefghi", 2, 4) + assert_equal ["\e[41m \e[42mい\e[43m ", 1, 4], Reline::Unicode.take_mbchar_range("\e[41mあ\e[42mい\e[43mう", 1, 4, padding: true) + end end diff --git a/test/reline/test_within_pipe.rb b/test/reline/test_within_pipe.rb index a42ca755fc..4f05255301 100644 --- a/test/reline/test_within_pipe.rb +++ b/test/reline/test_within_pipe.rb @@ -23,7 +23,6 @@ class Reline::WithinPipeTest < Reline::TestCase @reader.close @output_writer.close @config.reset - @config.reset_default_key_bindings Reline.test_reset end diff --git a/test/reline/yamatanooroti/multiline_repl b/test/reline/yamatanooroti/multiline_repl index e2a900b251..8b82be60f4 100755 --- a/test/reline/yamatanooroti/multiline_repl +++ b/test/reline/yamatanooroti/multiline_repl @@ -9,14 +9,10 @@ require 'optparse' require_relative 'termination_checker' opt = OptionParser.new -opt.on('--prompt-list-cache-timeout VAL') { |v| - Reline::LineEditor.__send__(:remove_const, :PROMPT_LIST_CACHE_TIMEOUT) - Reline::LineEditor::PROMPT_LIST_CACHE_TIMEOUT = v.to_f -} opt.on('--dynamic-prompt') { Reline.prompt_proc = proc { |lines| lines.each_with_index.map { |l, i| - '[%04d]> ' % i + "\e[1m[%04d]>\e[m " % i } } } @@ -147,12 +143,21 @@ opt.on('--complete') { %w{String ScriptError SyntaxError Signal}.select{ |c| c.start_with?(target) } } } +opt.on('--complete-menu-with-perfect-match') { + Reline.completion_proc = lambda { |target, preposing = nil, postposing = nil| + %w{abs abs2}.select{ |c| c.start_with?(target) } + } +} opt.on('--autocomplete') { Reline.autocompletion = true Reline.completion_proc = lambda { |target, preposing = nil, postposing = nil| %w{String Struct Symbol ScriptError SyntaxError Signal}.select{ |c| c.start_with?(target) } } } +opt.on('--autocomplete-empty') { + Reline.autocompletion = true + Reline.completion_proc = lambda { |target, preposing = nil, postposing = nil| [] } +} opt.on('--autocomplete-long') { Reline.autocompletion = true Reline.completion_proc = lambda { |target, preposing = nil, postposing = nil| @@ -183,7 +188,7 @@ opt.on('--autocomplete-long') { opt.on('--autocomplete-super-long') { Reline.autocompletion = true Reline.completion_proc = lambda { |target, preposing = nil, postposing = nil| - c = 'A' + c = +'A' 2000.times.map{ s = "Str_#{c}"; c.succ!; s }.select{ |c| c.start_with?(target) } } } @@ -217,7 +222,7 @@ rescue end begin - prompt = ENV['RELINE_TEST_PROMPT'] || 'prompt> ' + prompt = ENV['RELINE_TEST_PROMPT'] || "\e[1mprompt>\e[m " puts 'Multiline REPL.' while code = Reline.readmultiline(prompt, true) { |code| TerminationChecker.terminated?(code) } case code.chomp diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index 670432d86b..37a1c1a193 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -197,9 +197,12 @@ begin LINES start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') write(":a\n\C-[k") + write("i\n:a") + write("\C-[h") close assert_screen(<<~EOC) - Multiline REPL. + (ins)prompt> :a + => :a (ins)prompt> :a => :a (cmd)prompt> :a @@ -306,6 +309,21 @@ begin EOC end + def test_readline_with_multiline_input + start_terminal(5, 50, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --dynamic-prompt}, startup_message: 'Multiline REPL.') + write("def foo\n bar\nend\n") + write("Reline.readline('prompt> ')\n") + write("\C-p\C-p") + close + assert_screen(<<~EOC) + => :foo + [0000]> Reline.readline('prompt> ') + prompt> def foo + bar + end + EOC + end + def test_multiline_and_autowrap start_terminal(10, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') write("def aaaaaaaaaa\n 33333333\n end\C-a\C-pputs\C-e\e\C-m888888888888888") @@ -464,6 +482,9 @@ begin write("def a\n 8\nend\ndef b\n 3\nend\C-s8") close assert_screen(<<~EOC) + prompt> 8 + prompt> end + => :a (i-search)`8'def a (i-search)`8' 8 (i-search)`8'end @@ -475,6 +496,9 @@ begin write("def a\n 8\nend\ndef b\n 3\nend\C-r8\C-j") close assert_screen(<<~EOC) + prompt> 8 + prompt> end + => :a prompt> def a prompt> 8 prompt> end @@ -495,18 +519,6 @@ begin EOC end - def test_prompt_list_caching - start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --prompt-list-cache-timeout 10 --dynamic-prompt}, startup_message: 'Multiline REPL.') - write("def hoge\n 3\nend") - close - assert_screen(<<~EOC) - Multiline REPL. - [0000]> def hoge - [0001]> 3 - [0002]> end - EOC - end - def test_broken_prompt_list start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --broken-dynamic-prompt}, startup_message: 'Multiline REPL.') write("def hoge\n 3\nend") @@ -531,15 +543,10 @@ begin EOC end - def test_enable_bracketed_paste + def test_bracketed_paste omit if Reline.core.io_gate.win? - write_inputrc <<~LINES - set enable-bracketed-paste on - LINES start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') - write("\e[200~,") - write("def hoge\n 3\nend") - write("\e[200~.") + write("\e[200~def hoge\r\t3\rend\e[201~") close assert_screen(<<~EOC) Multiline REPL. @@ -549,6 +556,19 @@ begin EOC end + def test_bracketed_paste_with_undo + omit if Reline.core.io_gate.win? + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') + write("abc") + write("\e[200~def hoge\r\t3\rend\e[201~") + write("\C-_") + close + assert_screen(<<~EOC) + Multiline REPL. + prompt> abc + EOC + end + def test_backspace_until_returns_to_initial start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') write("ABC") @@ -834,6 +854,20 @@ begin EOC end + def test_auto_indent_with_various_spaces + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --auto-indent}, startup_message: 'Multiline REPL.') + write "(\n\C-v" + write "\C-k\n\C-v" + write "\C-k)" + close + assert_screen(<<~EOC) + Multiline REPL. + prompt> ( + prompt> ^K + prompt> ) + EOC + end + def test_autowrap_in_the_middle_of_a_line start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') write("def abcdefg; end\C-b\C-b\C-b\C-b\C-b") @@ -919,18 +953,30 @@ begin EOC end - def test_with_newline + def test_eof_with_newline omit if Reline.core.io_gate.win? cmd = %Q{ruby -e 'print(%Q{abc def \\e\\r})' | ruby -I#{@pwd}/lib -rreline -e 'p Reline.readline(%{> })'} start_terminal(40, 50, ['bash', '-c', cmd]) sleep 1 - close + close rescue nil assert_screen(<<~'EOC') > abc def "abc def " EOC end + def test_eof_without_newline + omit if Reline.core.io_gate.win? + cmd = %Q{ruby -e 'print(%{hello})' | ruby -I#{@pwd}/lib -rreline -e 'p Reline.readline(%{> })'} + start_terminal(40, 50, ['bash', '-c', cmd]) + sleep 1 + close rescue nil + assert_screen(<<~'EOC') + > hello + "hello" + EOC + end + def test_em_set_mark_and_em_exchange_mark start_terminal(10, 50, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') write("aaa bbb ccc ddd\M-b\M-b\M-\x20\M-b\C-x\C-xX\C-x\C-xY") @@ -980,6 +1026,47 @@ begin EOC end + def test_completion_menu_is_displayed_horizontally + start_terminal(20, 50, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --complete}, startup_message: 'Multiline REPL.') + write("S\t\t") + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> S + ScriptError String + Signal SyntaxError + EOC + end + + def test_show_all_if_ambiguous_on + write_inputrc <<~LINES + set show-all-if-ambiguous on + LINES + start_terminal(20, 50, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --complete}, startup_message: 'Multiline REPL.') + write("S\t") + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> S + ScriptError String + Signal SyntaxError + EOC + end + + def test_show_all_if_ambiguous_on_and_menu_with_perfect_match + write_inputrc <<~LINES + set show-all-if-ambiguous on + LINES + start_terminal(20, 50, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --complete-menu-with-perfect-match}, startup_message: 'Multiline REPL.') + write("a\t") + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> abs + abs abs2 + EOC + end + def test_simple_dialog iterate_over_face_configs do |config_name, config_file| start_terminal(20, 50, %W{ruby -I#{@pwd}/lib -r#{config_file.path} #{@pwd}/test/reline/yamatanooroti/multiline_repl --dialog simple}, startup_message: 'Multiline REPL.') @@ -1026,8 +1113,8 @@ begin iterate_over_face_configs do |config_name, config_file| start_terminal(10, 50, %W{ruby -I#{@pwd}/lib -r#{config_file.path} #{@pwd}/test/reline/yamatanooroti/multiline_repl --autocomplete}, startup_message: 'Multiline REPL.') write("\n" * 10) - write("if 1\n sSt\nend") - write("\C-p\C-h\C-e") + write("if 1\n sSts\nend") + write("\C-p\C-h\C-e\C-h") close assert_screen(<<~'EOC') prompt> @@ -1054,8 +1141,8 @@ begin prompt> 2 prompt> 3# prompt> 4 - prompt> 5 - prompt> 6 Ruby is... + prompt> 5 Ruby is... + prompt> 6 A dynamic, open source programming EOC end end @@ -1106,11 +1193,53 @@ begin assert_screen(<<~'EOC') Multiline REPL. prompt> St - r String + r + String + Struct + EOC + end + + def test_autocomplete_target_at_end_of_line + start_terminal(20, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --autocomplete}, startup_message: 'Multiline REPL.') + write(' ') + write('Str') + write("\C-i") + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> Str + ing String Struct EOC end + def test_autocomplete_completed_input_is_wrapped + start_terminal(20, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --autocomplete}, startup_message: 'Multiline REPL.') + write(' ') + write('Str') + write("\C-i") + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> Stri + ng String + Struct + EOC + end + + def test_force_insert_before_autocomplete + start_terminal(20, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --autocomplete}, startup_message: 'Multiline REPL.') + write('Sy') + write(";St\t\t") + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> Sy;Struct + String + Struct + EOC + end + def test_simple_dialog_with_scroll_key start_terminal(20, 50, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --dialog long,scrollkey}, startup_message: 'Multiline REPL.') write('a') @@ -1213,6 +1342,32 @@ begin EOC end + def test_autocomplete_empty_string + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --autocomplete}, startup_message: 'Multiline REPL.') + write("\C-i") + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> String + String █ + Struct ▀ + Symbol + EOC + end + + def test_paste_code_with_tab_indent_does_not_fail + start_terminal(5, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --autocomplete-empty}, startup_message: 'Multiline REPL.') + write("2.times do\n\tputs\n\tputs\nend") + close + assert_screen(<<~'EOC') + Multiline REPL. + prompt> 2.times do + prompt> puts + prompt> puts + prompt> end + EOC + end + def test_autocomplete_after_2nd_line start_terminal(20, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --autocomplete}, startup_message: 'Multiline REPL.') write("def hoge\n Str") @@ -1640,6 +1795,23 @@ begin EOC end + def test_thread_safe + start_terminal(6, 30, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --auto-indent}, startup_message: 'Multiline REPL.') + write("[Thread.new{Reline.readline'>'},Thread.new{Reline.readmultiline('>'){true}}].map(&:join).size\n") + write("exit\n") + write("exit\n") + write("42\n") + close + assert_screen(<<~EOC) + >exit + >exit + => 2 + prompt> 42 + => 42 + prompt> + EOC + end + def write_inputrc(content) File.open(@inputrc_file, 'w') do |f| f.write content |
