Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add copy command #1044

Merged
merged 1 commit into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/Configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Method `conf.rc?` returns `true` if a configuration file was read, `false` other
- `NO_COLOR`: Disables \IRB's colorization.
- `IRB_USE_AUTOCOMPLETE`: Setting to `false` disables autocompletion.
- `IRB_COMPLETOR`: Configures auto-completion behavior (`regexp` or `type`).
- `IRB_COPY_COMMAND`: Overrides the default program used to interface with the system clipboard.
- `VISUAL` / `EDITOR`: Specifies the editor for the `edit` command.
- `IRBRC`: Specifies the rc-file for configuration.
- `XDG_CONFIG_HOME`: Used to locate the rc-file if `IRBRC` is unset.
Expand Down
14 changes: 10 additions & 4 deletions lib/irb/color_printer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
module IRB
class ColorPrinter < ::PP
class << self
def pp(obj, out = $>, width = screen_width)
q = ColorPrinter.new(out, width)
def pp(obj, out = $>, width = screen_width, colorize: true)
q = ColorPrinter.new(out, width, colorize: colorize)
q.guard_inspect_key {q.pp obj}
q.flush
out << "\n"
Expand All @@ -21,6 +21,12 @@ def screen_width
end
end

def initialize(out, width, colorize: true)
@colorize = colorize

super(out, width)
end

def pp(obj)
if String === obj
# Avoid calling Ruby 2.4+ String#pretty_print that splits a string by "\n"
Expand All @@ -41,9 +47,9 @@ def text(str, width = nil)
when ',', '=>', '[', ']', '{', '}', '..', '...', /\A@\w+\z/
super(str, width)
when /\A#</, '=', '>'
super(Color.colorize(str, [:GREEN]), width)
super(@colorize ? Color.colorize(str, [:GREEN]) : str, width)
else
super(Color.colorize_code(str, ignore_error: true), width)
super(@colorize ? Color.colorize_code(str, ignore_error: true) : str, width)
end
end
end
Expand Down
63 changes: 63 additions & 0 deletions lib/irb/command/copy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

module IRB
module Command
class Copy < Base
category "Workspace"
description "Copy command output to clipboard"

help_message(<<~HELP)
Usage: copy [command]
HELP

def execute(arg)
# Copy last value if no expression was supplied
arg = '_' if arg.to_s.strip.empty?

value = irb_context.workspace.binding.eval(arg)
Prajjwal marked this conversation as resolved.
Show resolved Hide resolved
output = irb_context.inspect_method.inspect_value(value, colorize: false)

if clipboard_available?
copy_to_clipboard(output)
else
warn "System clipboard not found"
end
rescue StandardError => e
warn "Error: #{e}"
end

private

def copy_to_clipboard(text)
IO.popen(clipboard_program, 'w') do |io|
io.write(text)
end

raise IOError.new("Copying to clipboard failed") unless $? == 0

puts "Copied to system clipboard"
rescue Errno::ENOENT => e
warn e.message
warn "Is IRB.conf[:COPY_COMMAND] set to a bad value?"
end

def clipboard_program
@clipboard_program ||= if IRB.conf[:COPY_COMMAND]
IRB.conf[:COPY_COMMAND]
elsif executable?("pbcopy")
"pbcopy"
elsif executable?("xclip")
"xclip -selection clipboard"
end
end

def executable?(command)
system("which #{command} > /dev/null 2>&1")
end

def clipboard_available?
!!clipboard_program
end
end
end
end
2 changes: 2 additions & 0 deletions lib/irb/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ def irb_path=(path)
attr_reader :use_autocomplete
# A copy of the default <code>IRB.conf[:INSPECT_MODE]</code>
attr_reader :inspect_mode
# Inspector for the current context
attr_reader :inspect_method

# A copy of the default <code>IRB.conf[:PROMPT_MODE]</code>
attr_reader :prompt_mode
Expand Down
2 changes: 2 additions & 0 deletions lib/irb/default_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require_relative "command/chws"
require_relative "command/context"
require_relative "command/continue"
require_relative "command/copy"
require_relative "command/debug"
require_relative "command/delete"
require_relative "command/disable_irb"
Expand Down Expand Up @@ -250,6 +251,7 @@ def load_command(command)
)

register(:cd, Command::CD)
register(:copy, Command::Copy)
end

ExtendCommand = Command
Expand Down
2 changes: 2 additions & 0 deletions lib/irb/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ def IRB.init_config(ap_path)
:'$' => :show_source,
:'@' => :whereami,
}

@CONF[:COPY_COMMAND] = ENV.fetch("IRB_COPY_COMMAND", nil)
end

def IRB.set_measure_callback(type = nil, arg = nil, &block)
Expand Down
12 changes: 6 additions & 6 deletions lib/irb/inspector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ def init
end

# Proc to call when the input is evaluated and output in irb.
def inspect_value(v)
@inspect.call(v)
def inspect_value(v, colorize: true)
@inspect.call(v, colorize: colorize)
rescue => e
puts "An error occurred when inspecting the object: #{e.inspect}"

Expand All @@ -110,11 +110,11 @@ def inspect_value(v)
end

Inspector.def_inspector([false, :to_s, :raw]){|v| v.to_s}
Inspector.def_inspector([:p, :inspect]){|v|
Color.colorize_code(v.inspect, colorable: Color.colorable? && Color.inspect_colorable?(v))
Inspector.def_inspector([:p, :inspect]){|v, colorize: true|
Color.colorize_code(v.inspect, colorable: colorize && Color.colorable? && Color.inspect_colorable?(v))
}
Inspector.def_inspector([true, :pp, :pretty_inspect], proc{require_relative "color_printer"}){|v|
IRB::ColorPrinter.pp(v, +'').chomp
Inspector.def_inspector([true, :pp, :pretty_inspect], proc{require_relative "color_printer"}){|v, colorize: true|
IRB::ColorPrinter.pp(v, +'', colorize: colorize).chomp
}
Inspector.def_inspector([:yaml, :YAML], proc{require "yaml"}){|v|
begin
Expand Down
2 changes: 2 additions & 0 deletions man/irb.1
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ or
.Sy type
.
.Pp
.It Ev IRB_COPY_COMMAND
Overrides the default program used to interface with the system clipboard.
Comment on lines +230 to +231
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

screenshot

.El
.Pp
Also
Expand Down
70 changes: 70 additions & 0 deletions test/irb/command/test_copy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

require 'irb'

require_relative "../helper"

module TestIRB
class CopyTest < IntegrationTestCase
def setup
super
@envs['IRB_COPY_COMMAND'] = "ruby -e \"puts 'foo' + STDIN.read\""
end

def test_copy_with_pbcopy
write_ruby <<~'ruby'
class Answer
def initialize(answer)
@answer = answer
end
end

binding.irb
ruby

output = run_ruby_file do
type "copy Answer.new(42)"
type "exit"
end

assert_match(/foo#<Answer:0x[0-9a-f]+ @answer=42/, output)
assert_match(/Copied to system clipboard/, output)
end

# copy puts 5 should:
# - Print value to the console
# - Copy nil to clipboard, since that is what the puts call evaluates to
def test_copy_when_expression_has_side_effects
write_ruby <<~'ruby'
binding.irb
ruby

output = run_ruby_file do
type "copy puts 42"
type "exit"
end

assert_match(/^42\r\n/, output)
assert_match(/foonil/, output)
assert_match(/Copied to system clipboard/, output)
refute_match(/foo42/, output)
end

def test_copy_when_copy_command_is_invalid
@envs['IRB_COPY_COMMAND'] = "lulz"

write_ruby <<~'ruby'
binding.irb
ruby

output = run_ruby_file do
type "copy 42"
type "exit"
end

assert_match(/No such file or directory - lulz/, output)
assert_match(/Is IRB\.conf\[:COPY_COMMAND\] set to a bad value/, output)
refute_match(/Copied to system clipboard/, output)
end
end
end
13 changes: 13 additions & 0 deletions test/irb/test_color_printer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ def test_color_printer
end
end

def test_colorization_disabled
{
1 => "1\n",
"a\nb" => %["a\\nb"\n],
IRBTestColorPrinter.new('test') => "#<struct TestIRB::ColorPrinterTest::IRBTestColorPrinter a=\"test\">\n",
Ripper::Lexer.new('1').scan => "[#<Ripper::Lexer::Elem: on_int@1:0 END token: \"1\">]\n",
Class.new{define_method(:pretty_print){|q| q.text("[__FILE__, __LINE__, __ENCODING__]")}}.new => "[__FILE__, __LINE__, __ENCODING__]\n",
}.each do |object, result|
actual = with_term { IRB::ColorPrinter.pp(object, '', colorize: false) }
assert_equal(result, actual, "Case: IRB::ColorPrinter.pp(#{object.inspect}, '')")
end
end

private

def with_term
Expand Down
20 changes: 20 additions & 0 deletions test/irb/test_init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,26 @@ def test_use_autocomplete_environment_variable
IRB.conf[:USE_AUTOCOMPLETE] = orig_use_autocomplete_conf
end

def test_copy_command_environment_variable
orig_copy_command_env = ENV['IRB_COPY_COMMAND']
orig_copy_command_conf = IRB.conf[:COPY_COMMAND]

ENV['IRB_COPY_COMMAND'] = nil
IRB.setup(__FILE__)
refute IRB.conf[:COPY_COMMAND]

ENV['IRB_COPY_COMMAND'] = ''
IRB.setup(__FILE__)
assert_equal('', IRB.conf[:COPY_COMMAND])
Comment on lines +174 to +176
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure what the behavior here should be. Setting this env var overrides the other clipboard programs we use, so right now this would cause copy to stop working.

However, we could just as easily interpret '' to mean "fall back to pbcopy".


ENV['IRB_COPY_COMMAND'] = 'blah'
IRB.setup(__FILE__)
assert_equal('blah', IRB.conf[:COPY_COMMAND])
ensure
ENV['IRB_COPY_COMMAND'] = orig_copy_command_env
IRB.conf[:COPY_COMMAND] = orig_copy_command_conf
end

def test_completor_environment_variable
orig_use_autocomplete_env = ENV['IRB_COMPLETOR']
orig_use_autocomplete_conf = IRB.conf[:COMPLETOR]
Expand Down
Loading