Skip to content

Commit 15735b6

Browse files
author
José Valim
committed
Refactor IEx.{Server,Evaluator,Pry} APIs and test suites
1 parent b4c73b2 commit 15735b6

File tree

8 files changed

+631
-581
lines changed

8 files changed

+631
-581
lines changed

lib/iex/lib/iex.ex

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -440,12 +440,12 @@ defmodule IEx do
440440
the process information. Let's see an example:
441441
442442
import Enum, only: [map: 2]
443-
require IEx
443+
444444
445445
defmodule Adder do
446446
def add(a, b) do
447447
c = a + b
448-
IEx.pry
448+
require IEx; IEx.pry
449449
end
450450
end
451451
@@ -472,56 +472,12 @@ defmodule IEx do
472472
Setting variables or importing modules in IEx does not
473473
affect the caller's environment (hence it is called `pry`).
474474
"""
475-
defmacro pry(timeout \\ 5000) do
475+
defmacro pry() do
476476
quote do
477-
IEx.pry(binding(), __ENV__, unquote(timeout))
477+
IEx.Pry.pry(binding(), __ENV__)
478478
end
479479
end
480480

481-
@doc """
482-
Callback for `IEx.pry/1`.
483-
484-
You can invoke this function directly when you are not able to invoke
485-
`IEx.pry/1` as a macro. This function expects the binding (from
486-
`Kernel.binding/0`), the environment (from `__ENV__/0`) and the timeout
487-
(a sensible default is 5000).
488-
"""
489-
def pry(binding, env, timeout) do
490-
opts = [binding: binding, dot_iex_path: "", env: env, prefix: "pry"]
491-
meta = "#{inspect self()} at #{Path.relative_to_cwd(env.file)}:#{env.line}"
492-
desc =
493-
if File.regular?(env.file) do
494-
parse_file(env)
495-
else
496-
""
497-
end
498-
499-
res = IEx.Server.take_over("Request to pry #{meta}#{desc}", opts, timeout)
500-
501-
# We cannot use colors because IEx may be off.
502-
case res do
503-
{:error, :no_iex} ->
504-
extra =
505-
case :os.type do
506-
{:win32, _} -> " If you are Windows, you may need to start IEx with the --werl flag."
507-
_ -> ""
508-
end
509-
IO.puts :stdio, "Cannot pry #{meta}. Is an IEx shell running?" <> extra
510-
_ ->
511-
:ok
512-
end
513-
514-
res
515-
end
516-
517-
defp parse_file(env) do
518-
lines =
519-
env.file
520-
|> File.stream!
521-
|> Enum.slice(max(env.line - 3, 0), 5)
522-
Enum.intersperse(["\n\n" | lines], " ")
523-
end
524-
525481
## Callbacks
526482

527483
# This is a callback invoked by Erlang shell utilities

lib/iex/lib/iex/evaluator.ex

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ defmodule IEx.Evaluator do
1414
old_leader = Process.group_leader
1515
Process.group_leader(self(), leader)
1616

17-
state = loop_state(opts)
17+
state = loop_state(server, IEx.History.init, opts)
1818
command == :ack && :proc_lib.init_ack(self())
1919

2020
try do
21-
loop(server, IEx.History.init, state)
21+
loop(state)
2222
after
2323
Process.group_leader(self(), old_leader)
2424
end
@@ -71,23 +71,23 @@ defmodule IEx.Evaluator do
7171
end
7272
end
7373

74-
defp loop(server, history, state) do
74+
defp loop(%{server: server} = state) do
7575
receive do
7676
{:eval, ^server, code, iex_state} ->
77-
{result, history, state} = eval(code, iex_state, history, state)
77+
{result, state} = eval(code, iex_state, state)
7878
send server, {:evaled, self(), result}
79-
loop(server, history, state)
79+
loop(state)
8080
{:fields_from_env, ref, receiver, fields} ->
8181
send receiver, {ref, Map.take(state.env, fields)}
82-
loop(server, history, state)
82+
loop(state)
8383
{:value_from_binding, ref, receiver, var_name, map_key_path} ->
8484
value = traverse_binding(state.binding, var_name, map_key_path)
8585
send receiver, {ref, value}
86-
loop(server, history, state)
86+
loop(state)
8787
{:variables_from_binding, ref, receiver, var_prefix} ->
8888
value = find_matched_variables(state.binding, var_prefix)
8989
send receiver, {ref, value}
90-
loop(server, history, state)
90+
loop(state)
9191
{:done, ^server} ->
9292
:ok
9393
end
@@ -110,7 +110,7 @@ defmodule IEx.Evaluator do
110110
do: var_name
111111
end
112112

113-
defp loop_state(opts) do
113+
defp loop_state(server, history, opts) do
114114
env =
115115
if env = opts[:env] do
116116
:elixir.env_for_eval(env, [])
@@ -121,7 +121,7 @@ defmodule IEx.Evaluator do
121121
{_, _, env, scope} = :elixir.eval('import IEx.Helpers', [], env)
122122

123123
binding = Keyword.get(opts, :binding, [])
124-
state = %{binding: binding, scope: scope, env: env}
124+
state = %{binding: binding, scope: scope, env: env, server: server, history: history}
125125

126126
case opts[:dot_iex_path] do
127127
"" -> state
@@ -178,34 +178,38 @@ defmodule IEx.Evaluator do
178178
# https://github.com/elixir-lang/elixir/issues/1089 for discussion.
179179
@break_trigger '#iex:break\n'
180180

181-
defp eval(code, iex_state, history, state) do
181+
defp eval(code, iex_state, state) do
182182
try do
183-
do_eval(String.to_charlist(code), iex_state, history, state)
183+
do_eval(String.to_charlist(code), iex_state, state)
184184
catch
185185
kind, error ->
186186
print_error(kind, error, System.stacktrace)
187-
{%{iex_state | cache: ''}, history, state}
187+
{%{iex_state | cache: ''}, state}
188188
end
189189
end
190190

191-
defp do_eval(@break_trigger, %IEx.State{cache: ''} = iex_state, history, state) do
192-
{iex_state, history, state}
191+
defp do_eval(@break_trigger, %IEx.State{cache: ''} = iex_state, state) do
192+
{iex_state, state}
193193
end
194194

195-
defp do_eval(@break_trigger, iex_state, _history, _state) do
195+
defp do_eval(@break_trigger, iex_state, _state) do
196196
:elixir_errors.parse_error(iex_state.counter, "iex", "incomplete expression", "")
197197
end
198198

199-
defp do_eval(latest_input, iex_state, history, state) do
199+
defp do_eval(latest_input, iex_state, state) do
200200
code = iex_state.cache ++ latest_input
201201
line = iex_state.counter
202-
Process.put(:iex_history, history)
203-
handle_eval(Code.string_to_quoted(code, [line: line, file: "iex"]), code, line, iex_state, history, state)
202+
put_history(state)
203+
handle_eval(Code.string_to_quoted(code, [line: line, file: "iex"]), code, line, iex_state, state)
204204
after
205205
Process.delete(:iex_history)
206206
end
207207

208-
defp handle_eval({:ok, forms}, code, line, iex_state, history, state) do
208+
defp put_history(%{history: history}) do
209+
Process.put(:iex_history, history)
210+
end
211+
212+
defp handle_eval({:ok, forms}, code, line, iex_state, state) do
209213
{result, binding, env, scope} =
210214
:elixir.eval_forms(forms, state.binding, state.env, state.scope)
211215
unless result == IEx.dont_display_result, do: io_inspect(result)
@@ -218,22 +222,23 @@ defmodule IEx.Evaluator do
218222
scope: scope,
219223
binding: binding}
220224

221-
{iex_state, update_history(history, line, code, result), state}
225+
{iex_state, update_history(state, line, code, result)}
222226
end
223227

224-
defp handle_eval({:error, {_, _, ""}}, code, _line, iex_state, history, state) do
228+
defp handle_eval({:error, {_, _, ""}}, code, _line, iex_state, state) do
225229
# Update iex_state.cache so that IEx continues to add new input to
226230
# the unfinished expression in "code"
227-
{%{iex_state | cache: code}, history, state}
231+
{%{iex_state | cache: code}, state}
228232
end
229233

230-
defp handle_eval({:error, {line, error, token}}, _code, _line, _iex_state, _, _state) do
234+
defp handle_eval({:error, {line, error, token}}, _code, _line, _iex_state, _state) do
231235
# Encountered malformed expression
232236
:elixir_errors.parse_error(line, "iex", error, token)
233237
end
234238

235-
defp update_history(history, counter, cache, result) do
236-
IEx.History.append(history, {counter, cache, result}, IEx.Config.history_size)
239+
defp update_history(state, counter, cache, result) do
240+
history_size = IEx.Config.history_size
241+
update_in(state.history, &IEx.History.append(&1, {counter, cache, result}, history_size))
237242
end
238243

239244
defp io_inspect(result) do

lib/iex/lib/iex/helpers.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ defmodule IEx.Helpers do
4040
* `pwd/0` - prints the current working directory
4141
* `r/1` - recompiles the given module's source file
4242
* `recompile/0` - recompiles the current project
43-
* `respawn/0` - respawns the current shell
4443
* `system_info/0` - prints system info (versions, memory usage, stats)
4544
* `v/0` - retrieves the last value from the history
4645
* `v/1` - retrieves the nth value from the history

lib/iex/lib/iex/pry.ex

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
defmodule IEx.Pry do
2+
@moduledoc false
3+
4+
@doc """
5+
Callback for `IEx.pry/1`.
6+
7+
You can invoke this function directly when you are not able to invoke
8+
`IEx.pry/1` as a macro. This function expects the binding (from
9+
`Kernel.binding/0`) and the environment (from `__ENV__/0`).
10+
"""
11+
def pry(binding, env) do
12+
opts = [binding: binding, dot_iex_path: "", env: env, prefix: "pry"]
13+
meta = "#{inspect self()} at #{Path.relative_to_cwd(env.file)}:#{env.line}"
14+
desc =
15+
case whereami(env.file, env.line, 2) do
16+
{:ok, lines} -> [?\n, ?\n, lines]
17+
:error -> ""
18+
end
19+
20+
res = IEx.Server.take_over("Request to pry #{meta}#{desc}", opts)
21+
22+
# We cannot use colors because IEx may be off.
23+
case res do
24+
{:error, :no_iex} ->
25+
extra =
26+
case :os.type do
27+
{:win32, _} -> " If you are Windows, you may need to start IEx with the --werl flag."
28+
_ -> ""
29+
end
30+
IO.puts :stdio, "Cannot pry #{meta}. Is an IEx shell running?" <> extra
31+
_ ->
32+
:ok
33+
end
34+
35+
res
36+
end
37+
38+
@doc """
39+
Formats the location for whereami prying.
40+
"""
41+
def whereami(file, line, radius) do
42+
with true <- File.regular?(file),
43+
[_ | _] = lines <- whereami_lines(file, line, radius) do
44+
{:ok, lines}
45+
else
46+
_ -> :error
47+
end
48+
end
49+
50+
defp whereami_lines(file, line, radius) do
51+
min = max(line - radius - 1, 0)
52+
max = line + radius - 1
53+
54+
file
55+
|> File.stream!
56+
|> Enum.slice(min..max)
57+
|> Enum.with_index(min + 1)
58+
|> Enum.map(&whereami_format_line(&1, line))
59+
end
60+
61+
defp whereami_format_line({line_text, line_number}, line) do
62+
gutter = String.pad_leading(Integer.to_string(line_number), 5, " ")
63+
if line_number == line do
64+
IO.ANSI.format_fragment([:bright, gutter, ": ", line_text, :normal])
65+
else
66+
[gutter, ": ", line_text]
67+
end
68+
end
69+
end

0 commit comments

Comments
 (0)