1
0
Fork 0
mirror of https://github.com/pry/pry.git synced 2022-11-09 12:35:05 -05:00

prompt: add basic API for adding prompts

Fixes #1836 (Add an API for adding new prompts)
This commit is contained in:
Kyrylo Silin 2018-11-03 21:58:16 +08:00
parent 8588b36f62
commit 3da930f908
9 changed files with 170 additions and 124 deletions

View file

@ -185,11 +185,11 @@ Pry::CLI.add_options do
end
on "simple-prompt", "Enable simple prompt mode" do
Pry.config.prompt = Pry::Prompt::SIMPLE
Pry.config.prompt = Pry::Prompt[:simple][:value]
end
on "noprompt", "No prompt mode" do
Pry.config.prompt = Pry::Prompt::NO_PROMPT
Pry.config.prompt = Pry::Prompt[:none][:value]
end
on :r, :require=, "`require` a Ruby script at startup" do |file|

View file

@ -11,16 +11,12 @@ class Pry::Command::ChangePrompt < Pry::ClassCommand
BANNER
def process(prompt)
if prompt_map.key?(prompt)
_pry_.prompt = prompt_map[prompt][:value]
if Pry::Prompt.all.key?(prompt)
_pry_.prompt = Pry::Prompt.all[prompt][:value]
else
raise Pry::CommandError, "'#{prompt}' isn't a known prompt!"
end
end
private
def prompt_map
Pry::Prompt::MAP
end
Pry::Commands.add_command(self)
end

View file

@ -11,7 +11,7 @@ class Pry::Command::ListPrompts < Pry::ClassCommand
def process
output.puts heading("Available prompts") + "\n"
prompt_map.each do |name, prompt|
Pry::Prompt.all.each do |name, prompt|
output.write "Name: #{bold(name)}"
output.puts selected_prompt?(prompt) ? selected_text : ""
output.puts prompt[:description]
@ -19,10 +19,7 @@ class Pry::Command::ListPrompts < Pry::ClassCommand
end
end
private
def prompt_map
Pry::Prompt::MAP
end
private
def selected_text
red " (selected) "

View file

@ -9,13 +9,13 @@ class Pry
BANNER
def process
case _pry_.prompt
when Pry::Prompt::SHELL
_pry_.pop_prompt
_pry_.custom_completions = _pry_.config.file_completions
state.disabled ^= true
if state.disabled
state.prev_prompt = _pry_.prompt
_pry_.prompt = Pry::Prompt[:shell][:value]
else
_pry_.push_prompt Pry::Prompt::SHELL
_pry_.custom_completions = _pry_.config.command_completions
_pry_.prompt = state.prev_prompt
end
end
end

View file

@ -9,11 +9,13 @@ class Pry
BANNER
def process
case _pry_.prompt
when Pry::Prompt::SIMPLE
_pry_.pop_prompt
state.disabled ^= true
if state.disabled
state.prev_prompt = _pry_.prompt
_pry_.prompt = Pry::Prompt[:simple][:value]
else
_pry_.push_prompt Pry::Prompt::SIMPLE
_pry_.prompt = state.prev_prompt
end
end
end

View file

@ -18,7 +18,7 @@ class Pry
Pry::Prompt::DEFAULT_NAME
},
prompt: proc {
Pry::Prompt::DEFAULT
Pry::Prompt[:default][:value]
},
prompt_safe_contexts: proc {
Pry::Prompt::SAFE_CONTEXTS

View file

@ -1,9 +1,24 @@
class Pry
# Prompt represents the Pry prompt and holds necessary procs and constants to
# be used with Readline-like libraries.
# Prompt represents the Pry prompt, which can be used with Readline-like
# libraries. It defines a few default prompts (default prompt, simple prompt,
# etc) and also provides an API to add custom prompts.
#
# @example
# Pry::Prompt.add(
# :ipython,
# 'IPython-like prompt', [':', '...:']
# ) do |_context, _nesting, _pry_, sep|
# sep == ':' ? "In [#{_pry_.input_ring.count}]: " : ' ...: '
# end
#
# # Produces:
# # In [3]: def foo
# # ...: puts 'foo'
# # ...: end
# # => :foo
# # In [4]:
# @since v0.11.0
# @api private
# @api public
module Prompt
# @return [String]
DEFAULT_NAME = 'pry'.freeze
@ -12,60 +27,62 @@ class Pry
# 1-line #inspect output suitable for prompt
SAFE_CONTEXTS = [String, Numeric, Symbol, nil, true, false].freeze
# @return [String]
DEFAULT_TEMPLATE =
"[%<in_count>s] %<name>s(%<context>s)%<nesting>s%<separator>s ".freeze
# @return [String]
SHELL_TEMPLATE = "%<name>s %<context>s:%<pwd>s %<separator>s ".freeze
# @return [String]
NAV_TEMPLATE = "[%<in_count>s] (%<name>s) %<tree>s: %<stack_size>s> ".freeze
# A Hash that holds all prompts. The keys of the Hash are prompt
# names, the values are Hash instances of the format {:description, :value}.
@prompts = {}
class << self
# Retrieves a prompt.
#
# @example
# Prompt[:my_prompt][:value]
#
# @param [Symbol] prompt_name The name of the prompt you want to access
# @return [Hash{Symbol=>Object}]
# @since v0.12.0
def [](prompt_name)
all[prompt_name.to_s]
end
# @return [Hash{Symbol=>Hash}] the duplicate of the internal prompts hash
# @note Use this for read-only operations
# @since v0.12.0
def all
@prompts.dup
end
# Adds a new prompt to the prompt hash.
#
# @param [Symbol] prompt_name
# @param [String] description
# @param [Array<String>] separators The separators to differentiate
# between prompt modes (default mode and class/method definition mode).
# The Array *must* have a size of 2.
# @yield [context, nesting, _pry_, sep]
# @yieldparam context [Object] the context where Pry is currently in
# @yieldparam nesting [Integer] whether the context is nested
# @yieldparam _pry_ [Pry] the Pry instance
# @yieldparam separator [String] separator string
# @return [nil]
# @raise [ArgumentError] if the size of `separators` is not 2
# @since v0.12.0
def add(prompt_name, description = '', separators = %w[> *])
unless separators.size == 2
raise ArgumentError, "separators size must be 2, given #{separators.size}"
end
@prompts[prompt_name.to_s] = {
description: description,
value: separators.map do |sep|
proc { |context, nesting, _pry_| yield(context, nesting, _pry_, sep) }
end
}
nil
end
private
# @return [Proc] the default prompt
def default(separator)
proc do |context, nesting, _pry_|
format(
DEFAULT_TEMPLATE,
in_count: _pry_.input_ring.count,
name: prompt_name(_pry_.config.prompt_name),
context: Pry.view_clip(context),
nesting: (nesting > 0 ? ":#{nesting}" : ''),
separator: separator
)
end
end
# @return [Proc] the shell prompt
def shell(separator)
proc do |context, _nesting, _pry_|
format(
SHELL_TEMPLATE,
name: prompt_name(_pry_.config.prompt_name),
context: Pry.view_clip(context),
pwd: Dir.pwd,
separator: separator
)
end
end
# @return [Proc] the nav prompt
def nav
proc do |_context, _nesting, _pry_|
tree = _pry_.binding_stack.map { |b| Pry.view_clip(b.eval('self')) }
format(
NAV_TEMPLATE,
in_count: _pry_.input_ring.count,
name: prompt_name(_pry_.config.prompt_name),
tree: tree.join(' / '),
stack_size: _pry_.binding_stack.size - 1
)
end
end
def prompt_name(name)
return name unless name.is_a?(Pry::Config::Lazy)
@ -73,48 +90,53 @@ class Pry
end
end
# The default Pry prompt, which includes the context and nesting level.
# @return [Array<Proc>]
DEFAULT = [default('>'), default('*')].freeze
add(:default, <<DESC) do |context, nesting, _pry_, sep|
The default Pry prompt. Includes information about the
current expression number, evaluation context, and nesting
level, plus a reminder that you're using Pry.
DESC
format(
"[%<in_count>s] %<name>s(%<context>s)%<nesting>s%<separator>s ",
in_count: _pry_.input_ring.count,
name: prompt_name(_pry_.config.prompt_name),
context: Pry.view_clip(context),
nesting: (nesting > 0 ? ":#{nesting}" : ''),
separator: sep
)
end
# Simple prompt doesn't display target or nesting level.
# @return [Array<Proc>]
SIMPLE = [proc { '>> ' }, proc { ' | ' }].freeze
add(:simple, "A simple '>>'.", ['>> ', ' | ']) do |_, _, _, sep|
sep
end
# @return [Array<Proc>]
NO_PROMPT = Array.new(2) { proc { '' } }.freeze
add(:nav, <<DESC, %w[> *]) do |context, nesting, _pry_, sep|
A prompt that displays the binding stack as a path and
includes information about _in_ and _out_.
DESC
tree = _pry_.binding_stack.map { |b| Pry.view_clip(b.eval('self')) }
format(
"[%<in_count>s] (%<name>s) %<tree>s: %<stack_size>s%<separator>s ",
in_count: _pry_.input_ring.count,
name: prompt_name(_pry_.config.prompt_name),
tree: tree.join(' / '),
stack_size: _pry_.binding_stack.size - 1,
separator: sep
)
end
# @return [Array<Proc>]
SHELL = [shell('$'), shell('*')].freeze
add(:shell, <<DESC, %w[$ *]) do |context, nesting, _pry_, sep|
A prompt that displays the binding stack as a path and
includes information about _in_ and _out_.
DESC
format(
"%<name>s %<context>s:%<pwd>s %<separator>s ",
name: prompt_name(_pry_.config.prompt_name),
context: Pry.view_clip(context),
pwd: Dir.pwd,
separator: sep
)
end
# A prompt that includes the full object path as well as
# input/output (_in_ and _out_) information. Good for navigation.
NAV = Array.new(2) { nav }.freeze
# @return [Hash{String=>Hash}]
MAP = {
"default" => {
value: DEFAULT,
description: "The default Pry prompt. Includes information about the\n" \
"current expression number, evaluation context, and nesting\n" \
"level, plus a reminder that you're using Pry.".freeze
},
"simple" => {
value: SIMPLE,
description: "A simple '>>'.".freeze
},
"nav" => {
value: NAV,
description: "A prompt that displays the binding stack as a path and\n" \
"includes information about _in_ and _out_.".freeze
},
"none" => {
value: NO_PROMPT,
description: "Wave goodbye to the Pry prompt.".freeze
}
}.freeze
add(:none, 'Wave goodbye to the Pry prompt.', Array.new(2)) { '' }
end
end

View file

@ -117,14 +117,8 @@ class Pry
@output_ring
end
# The current prompt.
# This is the prompt at the top of the prompt stack.
#
# @example
# self.prompt = Pry::Prompt::SIMPLE
# self.prompt # => Pry::Prompt::SIMPLE
#
# @return [Array<Proc>] Current prompt.
# @return [Array<Proc>] the current prompt
def prompt
prompt_stack.last
end

View file

@ -1,6 +1,41 @@
require_relative 'helper'
describe Pry::Prompt do
describe ".[]" do
it "accesses prompts" do
expect(subject[:default]).not_to be_nil
end
end
describe ".all" do
it "returns a hash with prompts" do
expect(subject.all).to be_a(Hash)
end
it "returns a duplicate of original prompts" do
subject.all[:foobar] = Object.new
expect(subject[:foobar]).to be_nil
end
end
describe ".add" do
after { described_class.instance_variable_get(:@prompts).delete(:my_prompt) }
it "adds a new prompt" do
subject.add(:my_prompt)
expect(subject[:my_prompt]).to be_a(Hash)
end
it "raises error when separators.size != 2" do
expect { subject.add(:my_prompt, '', [1, 2, 3]) }
.to raise_error(ArgumentError)
end
it "returns nil" do
expect(subject.add(:my_prompt)).to be_nil
end
end
describe "one-parameter prompt proc" do
it 'should get full config object' do
config = nil
@ -80,7 +115,7 @@ describe Pry::Prompt do
end
config._pry_.config.prompt_name = Pry.lazy { enum.next }
proc = subject::DEFAULT.first
proc = subject[:default][:value].first
expect(proc.call(Object.new, 1, config._pry_)).to eq('[1] 101(#<Object>):1> ')
expect(proc.call(Object.new, 1, config._pry_)).to eq('[1] 102(#<Object>):1> ')
end