Skip to content

Commit 8c8e8b1

Browse files
author
José Valim
committed
Merge pull request #1121 from alco/iex-limit-history
Iex limit history
2 parents dad7f7b + 3aaaf5e commit 8c8e8b1

File tree

5 files changed

+192
-34
lines changed

5 files changed

+192
-34
lines changed

lib/iex/lib/iex/helpers.ex

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,6 @@ defmodule IEx.Helpers do
7676
end
7777
end
7878

79-
@doc """
80-
Prints the history of expressions evaluated during the session along with
81-
their results.
82-
"""
83-
def v do
84-
history = Enum.reverse(Process.get(:iex_history))
85-
Enum.each(history, print_history(&1))
86-
end
87-
88-
defp print_history(config) do
89-
IO.puts IEx.color(:info, "#{config.counter}: #{config.cache}#=> #{inspect config.result}\n")
90-
end
91-
9279
@doc """
9380
Prints the documentation for `IEx.Helpers`.
9481
"""
@@ -217,20 +204,28 @@ defmodule IEx.Helpers do
217204
end
218205
end
219206

207+
@doc """
208+
Prints the history of expressions evaluated during the session along with
209+
their results.
210+
"""
211+
def v do
212+
inspect_opts = IEx.Options.get(:inspect)
213+
IEx.History.each(print_history_entry(&1, inspect_opts))
214+
end
215+
216+
defp print_history_entry(config, inspect_opts) do
217+
IO.write IEx.color(:info, "#{config.counter}: #{config.cache}#=> ")
218+
IO.puts IEx.color(:eval_result, "#{inspect config.result, inspect_opts}\n")
219+
end
220+
220221
@doc """
221222
Retrieves nth expression's value from the history.
222223
223224
Use negative values to lookup expression values relative to the current one.
224225
For instance, v(-1) returns the result of the last evaluated expression.
225226
"""
226-
def v(n) when n < 0 do
227-
history = Process.get(:iex_history)
228-
Enum.fetch!(history, abs(n) - 1).result
229-
end
230-
231-
def v(n) when n > 0 do
232-
history = Process.get(:iex_history) |> Enum.reverse
233-
Enum.fetch!(history, n - 1).result
227+
def v(n) do
228+
IEx.History.nth(n).result
234229
end
235230

236231
@doc """
@@ -268,10 +263,15 @@ defmodule IEx.Helpers do
268263
Flushes all messages sent to the shell and prints them out.
269264
"""
270265
def flush do
266+
inspect_opts = IEx.Options.get(:inspect)
267+
do_flush(inspect_opts)
268+
end
269+
270+
defp do_flush(inspect_opts) do
271271
receive do
272272
msg ->
273-
IO.inspect(msg)
274-
flush
273+
IO.inspect(msg, inspect_opts)
274+
do_flush(inspect_opts)
275275
after
276276
0 -> :ok
277277
end

lib/iex/lib/iex/history.ex

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
defmodule IEx.History do
2+
@moduledoc false
3+
4+
def init do
5+
Process.put(:iex_history_start_counter, 1)
6+
Process.put(:iex_history_counter, 1)
7+
end
8+
9+
### append ###
10+
11+
def append(entry, counter) do
12+
Process.put({:iex_history, counter}, entry)
13+
Process.put(:iex_history_counter, counter+1)
14+
15+
limit = IEx.Options.get(:history_size)
16+
start_counter = Process.get(:iex_history_start_counter)
17+
should_collect = limit_history(start_counter, counter, limit, false)
18+
if should_collect do
19+
collect_garbage()
20+
end
21+
end
22+
23+
defp limit_history(_, _, limit, _) when limit < 0 do
24+
false
25+
end
26+
27+
defp limit_history(counter, max_counter, limit, should_collect) when max_counter - counter < limit do
28+
Process.put(:iex_history_start_counter, counter)
29+
should_collect
30+
end
31+
32+
defp limit_history(counter, max_counter, limit, should_collect) do
33+
if not should_collect do
34+
entry = Process.delete({:iex_history, counter})
35+
should_collect = has_binary(entry.result)
36+
else
37+
Process.delete({:iex_history, counter})
38+
end
39+
limit_history(counter+1, max_counter, limit, should_collect)
40+
end
41+
42+
# Checks val and each of its elements (if it is a list or a tuple)
43+
# recursively to see if it has any binaries
44+
defp has_binary(val) do
45+
try do
46+
has_bin(val)
47+
catch
48+
:throw, true -> true
49+
end
50+
end
51+
52+
# Worker function used by has_binary. Throws when the first binary of the
53+
# minimum specified size is found
54+
defp has_bin(val) when is_tuple(val) do
55+
has_bin(val, tuple_size(val)-1)
56+
end
57+
58+
defp has_bin([h|t]) do
59+
has_bin(h)
60+
has_bin(t)
61+
end
62+
63+
defp has_bin(val) when byte_size(val) > 64 do
64+
throw true
65+
end
66+
67+
defp has_bin(_) do
68+
false
69+
end
70+
71+
defp has_bin(_, -1) do
72+
false
73+
end
74+
75+
defp has_bin(tuple, index) do
76+
has_bin(elem(tuple, index))
77+
has_bin(tuple, index-1)
78+
end
79+
80+
# Based on https://github.com/erlang/otp/blob/7dcccee4371477e983f026db9e243cb66900b1ef/lib/stdlib/src/shell.erl#L1401
81+
defp collect_garbage do
82+
:erlang.garbage_collect(self())
83+
try do
84+
:erlang.garbage_collect(Process.whereis(:user))
85+
catch
86+
_, _ -> nil
87+
end
88+
try do
89+
:erlang.garbage_collect(Process.group_leader())
90+
catch
91+
_, _ -> nil
92+
end
93+
:erlang.garbage_collect()
94+
end
95+
96+
### each ###
97+
98+
def each(fun) do
99+
each(Process.get(:iex_history_start_counter),
100+
Process.get(:iex_history_counter),
101+
fun)
102+
end
103+
104+
defp each(counter, max_counter, fun) when counter < max_counter do
105+
entry = Process.get({:iex_history, counter})
106+
fun.(entry)
107+
each(counter+1, max_counter, fun)
108+
end
109+
110+
defp each(_, _, _) do
111+
:ok
112+
end
113+
114+
### nth ###
115+
116+
def nth(n) do
117+
entry = case n do
118+
n when n >= 0 ->
119+
Process.get({:iex_history, n})
120+
n when n < 0 ->
121+
counter = Process.get(:iex_history_counter)
122+
Process.get({:iex_history, counter + n})
123+
end
124+
if nil?(entry) do
125+
raise "Out of bounds"
126+
end
127+
entry
128+
end
129+
end

lib/iex/lib/iex/options.ex

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ defmodule IEx.Options do
3636
@supported_options [
3737
colors: [],
3838
inspect: [],
39+
history_size: [],
3940
]
4041

4142
@doc """
@@ -64,6 +65,11 @@ defmodule IEx.Options do
6465
opts
6566
end
6667

68+
def get(:history_size) do
69+
{ :ok, size } = :application.get_env(:iex, :history_size)
70+
size
71+
end
72+
6773
def get(name) do
6874
raise_option(name)
6975
end
@@ -97,7 +103,7 @@ defmodule IEx.Options do
97103
end
98104

99105
def set(:colors, _) do
100-
raise_value
106+
raise_value("a keyword list")
101107
end
102108

103109
def set(:inspect, opts) when is_list(opts) do
@@ -108,7 +114,17 @@ defmodule IEx.Options do
108114
end
109115

110116
def set(:inspect, _) do
111-
raise_value
117+
raise_value("a keyword list")
118+
end
119+
120+
def set(:history_size, size) when is_integer(size) do
121+
old_size = get(:history_size)
122+
:application.set_env(:iex, :history_size, size)
123+
old_size
124+
end
125+
126+
def set(:history_size, _) do
127+
raise_value("an integer")
112128
end
113129

114130
def set(name, _) do
@@ -187,12 +203,24 @@ defmodule IEx.Options do
187203
"""
188204
def inspect
189205

206+
@doc """
207+
**NOTE**: This is just a stub for documentation purposes. Use
208+
`IEx.Options.get` and `IEx.Options.set` to query and change the option's
209+
value.
210+
211+
Number of expressions and their results to keep in the history.
212+
213+
The value is an integer. When it is negative, the history is unlimited.
214+
215+
"""
216+
def history_size
217+
190218
defp raise_option(name) do
191219
raise ArgumentError, message: "Unknown IEx option #{inspect name}"
192220
end
193221

194-
defp raise_value do
195-
raise ArgumentError, message: "Expected the value to be a keyword list"
222+
defp raise_value(type) do
223+
raise ArgumentError, message: "Expected the value to be #{type}"
196224
end
197225

198226
defp filtered_kw(reference_kw, user_kw) do

lib/iex/lib/iex/server.ex

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ defmodule IEx.Server do
44
@doc """
55
Eval loop for an IEx session. Its responsibilities include:
66
7+
* loading of .iex files
78
* reading input
89
* trapping exceptions in the code being evaluated
9-
* keeping input history
10+
* keeping expression history
1011
1112
"""
1213
def start(config) do
13-
Process.put :iex_history, []
14+
IEx.History.init
1415

1516
{ _, _, scope } = :elixir.eval('require IEx.Helpers', [], 0, config.scope)
1617
config = config.scope(scope)
@@ -81,9 +82,9 @@ defmodule IEx.Server do
8182

8283
io_put result
8384

84-
config = config.result(result)
85-
update_history(config.cache(code).scope(nil))
86-
config.update_counter(&1+1).cache('').binding(new_binding).scope(scope)
85+
config = config.cache(code).scope(nil).result(result)
86+
update_history(config)
87+
config.update_counter(&1+1).cache('').binding(new_binding).scope(scope).result(nil)
8788

8889
{ :error, { line_no, error, token } } ->
8990
if token == [] do
@@ -135,8 +136,7 @@ defmodule IEx.Server do
135136
end
136137

137138
defp update_history(config) do
138-
current = Process.get :iex_history
139-
Process.put :iex_history, [config|current]
139+
IEx.History.append(config, config.counter)
140140
end
141141

142142
defp io_get(config) do

lib/iex/mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ defmodule IEx.Mixfile do
1717
directory: "blue",
1818
device: "green"
1919
],
20+
history_size: 20,
2021
started: true
2122
]]
2223
end

0 commit comments

Comments
 (0)