mirror of
https://github.com/ruby/ruby.git
synced 2025-09-15 08:33:58 +02:00
[ruby/irb] Command implementation not by method
(https://github.com/ruby/irb/pull/824)
* Command is not a method
* Fix command test
* Implement non-method command name completion
* Add test for ExtendCommandBundle.def_extend_command
* Add helper method install test
* Remove spaces in command input parse
* Remove command arg unquote in help command
* Simplify Statement and handle execution in IRB::Irb
* Tweak require, const name
* Always install CommandBundle module to main object
* Remove considering local variable in command or expression check
* Remove unused method, tweak
* Remove outdated comment for help command arg
Co-authored-by: Stan Lo <stan001212@gmail.com>
---------
8fb776e379
Co-authored-by: Stan Lo <stan001212@gmail.com>
This commit is contained in:
parent
9f6deaa688
commit
6a505d1b59
36 changed files with 414 additions and 328 deletions
|
@ -7,12 +7,8 @@ module IRB
|
|||
|
||||
module Command
|
||||
class Backtrace < DebugCommand
|
||||
def self.transform_args(args)
|
||||
args&.dump
|
||||
end
|
||||
|
||||
def execute(*args)
|
||||
super(pre_cmds: ["backtrace", *args].join(" "))
|
||||
def execute(arg)
|
||||
execute_debug_command(pre_cmds: "backtrace #{arg}".rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,10 @@ module IRB
|
|||
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)
|
||||
|
@ -29,19 +33,13 @@ module IRB
|
|||
|
||||
private
|
||||
|
||||
def string_literal?(args)
|
||||
sexp = Ripper.sexp(args)
|
||||
sexp && sexp.size == 2 && sexp.last&.first&.first == :string_literal
|
||||
end
|
||||
|
||||
def highlight(text)
|
||||
Color.colorize(text, [:BOLD, :BLUE])
|
||||
end
|
||||
end
|
||||
|
||||
def self.execute(irb_context, *opts, **kwargs, &block)
|
||||
command = new(irb_context)
|
||||
command.execute(*opts, **kwargs, &block)
|
||||
def self.execute(irb_context, arg)
|
||||
new(irb_context).execute(arg)
|
||||
rescue CommandArgumentError => e
|
||||
puts e.message
|
||||
end
|
||||
|
@ -52,7 +50,26 @@ module IRB
|
|||
|
||||
attr_reader :irb_context
|
||||
|
||||
def execute(*opts)
|
||||
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
|
||||
|
||||
def execute(arg)
|
||||
#nop
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,12 +7,8 @@ module IRB
|
|||
|
||||
module Command
|
||||
class Break < DebugCommand
|
||||
def self.transform_args(args)
|
||||
args&.dump
|
||||
end
|
||||
|
||||
def execute(args = nil)
|
||||
super(pre_cmds: "break #{args}")
|
||||
def execute(arg)
|
||||
execute_debug_command(pre_cmds: "break #{arg}".rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,12 +7,8 @@ module IRB
|
|||
|
||||
module Command
|
||||
class Catch < DebugCommand
|
||||
def self.transform_args(args)
|
||||
args&.dump
|
||||
end
|
||||
|
||||
def execute(*args)
|
||||
super(pre_cmds: ["catch", *args].join(" "))
|
||||
def execute(arg)
|
||||
execute_debug_command(pre_cmds: "catch #{arg}".rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ module IRB
|
|||
category "Workspace"
|
||||
description "Show the current workspace."
|
||||
|
||||
def execute(*obj)
|
||||
def execute(_arg)
|
||||
irb_context.main
|
||||
end
|
||||
end
|
||||
|
@ -23,8 +23,13 @@ module IRB
|
|||
category "Workspace"
|
||||
description "Change the current workspace to an object."
|
||||
|
||||
def execute(*obj)
|
||||
irb_context.change_workspace(*obj)
|
||||
def execute(arg)
|
||||
if arg.empty?
|
||||
irb_context.change_workspace
|
||||
else
|
||||
obj = eval(arg, irb_context.workspace.binding)
|
||||
irb_context.change_workspace(obj)
|
||||
end
|
||||
irb_context.main
|
||||
end
|
||||
end
|
||||
|
|
16
lib/irb/command/context.rb
Normal file
16
lib/irb/command/context.rb
Normal file
|
@ -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
|
|
@ -7,8 +7,8 @@ module IRB
|
|||
|
||||
module Command
|
||||
class Continue < DebugCommand
|
||||
def execute(*args)
|
||||
super(do_cmds: ["continue", *args].join(" "))
|
||||
def execute(arg)
|
||||
execute_debug_command(do_cmds: "continue #{arg}".rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,11 @@ module IRB
|
|||
binding.method(:irb).source_location.first,
|
||||
].map { |file| /\A#{Regexp.escape(file)}:\d+:in (`|'Binding#)irb'\z/ }
|
||||
|
||||
def execute(pre_cmds: nil, do_cmds: nil)
|
||||
def execute(_arg)
|
||||
execute_debug_command
|
||||
end
|
||||
|
||||
def execute_debug_command(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
|
||||
|
|
|
@ -7,8 +7,8 @@ module IRB
|
|||
|
||||
module Command
|
||||
class Delete < DebugCommand
|
||||
def execute(*args)
|
||||
super(pre_cmds: ["delete", *args].join(" "))
|
||||
def execute(arg)
|
||||
execute_debug_command(pre_cmds: "delete #{arg}".rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,19 +26,9 @@ module IRB
|
|||
edit Foo#bar
|
||||
HELP_MESSAGE
|
||||
|
||||
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
|
||||
def execute(arg)
|
||||
# Accept string literal for backward compatibility
|
||||
path = unwrap_string_literal(arg)
|
||||
|
||||
if path.nil?
|
||||
path = @irb_context.irb_path
|
||||
|
|
|
@ -7,8 +7,8 @@ module IRB
|
|||
|
||||
module Command
|
||||
class Finish < DebugCommand
|
||||
def execute(*args)
|
||||
super(do_cmds: ["finish", *args].join(" "))
|
||||
def execute(arg)
|
||||
execute_debug_command(do_cmds: "finish #{arg}".rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,27 +6,16 @@ module IRB
|
|||
category "Help"
|
||||
description "List all available commands. Use `help <command>` to get information about a specific command."
|
||||
|
||||
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(command_name = nil)
|
||||
def execute(command_name)
|
||||
content =
|
||||
if command_name
|
||||
if command_name.empty?
|
||||
help_message
|
||||
else
|
||||
if command_class = ExtendCommandBundle.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
|
||||
else
|
||||
help_message
|
||||
end
|
||||
Pager.page_content(content)
|
||||
end
|
||||
|
|
|
@ -12,14 +12,12 @@ module IRB
|
|||
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)
|
||||
|
||||
|
|
|
@ -7,12 +7,8 @@ module IRB
|
|||
|
||||
module Command
|
||||
class Info < DebugCommand
|
||||
def self.transform_args(args)
|
||||
args&.dump
|
||||
end
|
||||
|
||||
def execute(*args)
|
||||
super(pre_cmds: ["info", *args].join(" "))
|
||||
def execute(arg)
|
||||
execute_debug_command(pre_cmds: "info #{arg}".rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ module IRB
|
|||
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"
|
||||
|
|
|
@ -21,7 +21,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
|
||||
|
@ -30,7 +35,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)?")
|
||||
|
@ -63,7 +74,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)
|
||||
|
|
|
@ -20,27 +20,35 @@ module IRB
|
|||
-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
|
||||
|
||||
|
|
|
@ -10,15 +10,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
|
||||
|
|
|
@ -7,8 +7,8 @@ module IRB
|
|||
|
||||
module Command
|
||||
class Next < DebugCommand
|
||||
def execute(*args)
|
||||
super(do_cmds: ["next", *args].join(" "))
|
||||
def execute(arg)
|
||||
execute_debug_command(do_cmds: "next #{arg}".rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ module IRB
|
|||
category "Workspace"
|
||||
description "Show workspaces."
|
||||
|
||||
def execute(*obj)
|
||||
def execute(_arg)
|
||||
inspection_resuls = irb_context.instance_variable_get(:@workspace_stack).map do |ws|
|
||||
truncated_inspect(ws.main)
|
||||
end
|
||||
|
@ -39,8 +39,13 @@ module IRB
|
|||
category "Workspace"
|
||||
description "Push an object to the workspace stack."
|
||||
|
||||
def execute(*obj)
|
||||
irb_context.push_workspace(*obj)
|
||||
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
|
||||
|
@ -49,8 +54,8 @@ module IRB
|
|||
category "Workspace"
|
||||
description "Pop a workspace from the workspace stack."
|
||||
|
||||
def execute(*obj)
|
||||
irb_context.pop_workspace(*obj)
|
||||
def execute(_arg)
|
||||
irb_context.pop_workspace
|
||||
super
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,17 +3,6 @@
|
|||
module IRB
|
||||
module Command
|
||||
class ShowDoc < Base
|
||||
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 "Look up documentation with RI."
|
||||
|
||||
|
@ -31,7 +20,9 @@ module IRB
|
|||
|
||||
HELP_MESSAGE
|
||||
|
||||
def execute(*names)
|
||||
def execute(arg)
|
||||
# Accept string literal for backward compatibility
|
||||
name = unwrap_string_literal(arg)
|
||||
require 'rdoc/ri/driver'
|
||||
|
||||
unless ShowDoc.const_defined?(:Ri)
|
||||
|
@ -39,15 +30,13 @@ module IRB
|
|||
ShowDoc.const_set(:Ri, RDoc::RI::Driver.new(opts))
|
||||
end
|
||||
|
||||
if names.empty?
|
||||
if name.nil?
|
||||
Ri.interactive
|
||||
else
|
||||
names.each do |name|
|
||||
begin
|
||||
Ri.display_name(name.to_s)
|
||||
rescue RDoc::RI::Error
|
||||
puts $!.message
|
||||
end
|
||||
begin
|
||||
Ri.display_name(name)
|
||||
rescue RDoc::RI::Error
|
||||
puts $!.message
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -24,18 +24,9 @@ module IRB
|
|||
show_source Foo::BAR
|
||||
HELP_MESSAGE
|
||||
|
||||
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)
|
||||
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
|
||||
|
|
|
@ -7,8 +7,8 @@ module IRB
|
|||
|
||||
module Command
|
||||
class Step < DebugCommand
|
||||
def execute(*args)
|
||||
super(do_cmds: ["step", *args].join(" "))
|
||||
def execute(arg)
|
||||
execute_debug_command(do_cmds: "step #{arg}".rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,10 +9,6 @@ module IRB
|
|||
|
||||
module Command
|
||||
class MultiIRBCommand < Base
|
||||
def execute(*args)
|
||||
extend_irb_context
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def print_deprecated_warning
|
||||
|
@ -36,7 +32,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
|
||||
|
@ -44,7 +45,7 @@ module IRB
|
|||
return
|
||||
end
|
||||
|
||||
super
|
||||
extend_irb_context
|
||||
IRB.irb(nil, *obj)
|
||||
end
|
||||
end
|
||||
|
@ -53,7 +54,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
|
||||
|
@ -61,7 +62,7 @@ module IRB
|
|||
return
|
||||
end
|
||||
|
||||
super
|
||||
extend_irb_context
|
||||
IRB.JobManager
|
||||
end
|
||||
end
|
||||
|
@ -70,7 +71,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
|
||||
|
@ -78,7 +84,7 @@ 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)
|
||||
|
@ -89,7 +95,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
|
||||
|
@ -97,7 +108,7 @@ module IRB
|
|||
return
|
||||
end
|
||||
|
||||
super
|
||||
extend_irb_context
|
||||
IRB.JobManager.kill(*keys)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ module IRB
|
|||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue