Skip to content

Commit 1d054b6

Browse files
committed
Embed IEx parser into get_until
1 parent 7de6031 commit 1d054b6

File tree

8 files changed

+110
-113
lines changed

8 files changed

+110
-113
lines changed

lib/iex/lib/iex.ex

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -482,14 +482,8 @@ defmodule IEx do
482482
483483
* `:default_prompt` - used when `Node.alive?/0` returns `false`
484484
485-
* `:continuation_prompt` - used when `Node.alive?/0` returns `false`
486-
and more input is expected
487-
488485
* `:alive_prompt` - used when `Node.alive?/0` returns `true`
489486
490-
* `:alive_continuation_prompt` - used when `Node.alive?/0` returns
491-
`true` and more input is expected
492-
493487
The following values in the prompt string will be replaced appropriately:
494488
495489
* `%counter` - the index of the history
@@ -503,11 +497,17 @@ defmodule IEx do
503497
The parser is a "mfargs", which is a tuple with three elements:
504498
the module name, the function name, and extra arguments to
505499
be appended. The parser receives at least three arguments, the
506-
current input as a string, the parsing options as a keyword list,
507-
and the buffer as a string. It must return `{:ok, expr, buffer}`
508-
or `{:incomplete, buffer}`.
500+
current input as a charlist, the parsing options as a keyword list,
501+
and the state. The initial state is an empty charlist. It must
502+
return `{:ok, expr, state}` or `{:incomplete, state}`.
503+
504+
If the parser raises, the state is reset to an empty charlist.
509505
510-
If the parser raises, the buffer is reset to an empty string.
506+
> In earlier Elixir versions, the parser would receive the input
507+
> and the initial buffer as strings. However, this behaviour
508+
> changed when Erlang/OTP introduced multiline editing. If you
509+
> support earlier Elixir versions, you can normalize the inputs
510+
> by calling `to_charlist/1`.
511511
"""
512512
@spec configure(keyword()) :: :ok
513513
def configure(options) do

lib/iex/lib/iex/app.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ defmodule IEx.App do
44
use Application
55

66
def start(_type, _args) do
7+
with :default <- Application.get_env(:stdlib, :shell_multiline_prompt, :default) do
8+
Application.put_env(:stdlib, :shell_multiline_prompt, {IEx.Config, :prompt})
9+
end
10+
711
children = [IEx.Config, IEx.Broker, IEx.Pry]
812
Supervisor.start_link(children, strategy: :one_for_one, name: IEx.Supervisor)
913
end

lib/iex/lib/iex/config.ex

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,24 @@ defmodule IEx.Config do
99
:inspect,
1010
:history_size,
1111
:default_prompt,
12-
:continuation_prompt,
1312
:alive_prompt,
14-
:alive_continuation_prompt,
1513
:width,
1614
:parser
1715
]
1816

17+
# Generate a continuation prompt based on IEx prompt.
18+
# This is set as global configuration on app start.
19+
def prompt(prompt) do
20+
case Enum.split_while(prompt, &(&1 != ?()) do
21+
# It is not the default Elixir shell, so we use the default prompt
22+
{_, []} ->
23+
List.duplicate(?\s, max(0, :prim_tty.npwcwidthstring(prompt) - 3)) ++ ~c".. "
24+
25+
{left, right} ->
26+
List.duplicate(?., :prim_tty.npwcwidthstring(left)) ++ right
27+
end
28+
end
29+
1930
# Read API
2031

2132
def configuration() do
@@ -47,20 +58,12 @@ defmodule IEx.Config do
4758
Application.fetch_env!(:iex, :default_prompt)
4859
end
4960

50-
def continuation_prompt() do
51-
Application.get_env(:iex, :continuation_prompt, default_prompt())
52-
end
53-
5461
def alive_prompt() do
5562
Application.fetch_env!(:iex, :alive_prompt)
5663
end
5764

58-
def alive_continuation_prompt() do
59-
Application.get_env(:iex, :alive_continuation_prompt, alive_prompt())
60-
end
61-
6265
def parser() do
63-
Application.get_env(:iex, :parser, {IEx.Evaluator, :parse, []})
66+
Application.fetch_env!(:iex, :parser)
6467
end
6568

6669
def color(color) do
@@ -188,9 +191,7 @@ defmodule IEx.Config do
188191
defp validate_option({:inspect, new}) when is_list(new), do: :ok
189192
defp validate_option({:history_size, new}) when is_integer(new), do: :ok
190193
defp validate_option({:default_prompt, new}) when is_binary(new), do: :ok
191-
defp validate_option({:continuation_prompt, new}) when is_binary(new), do: :ok
192194
defp validate_option({:alive_prompt, new}) when is_binary(new), do: :ok
193-
defp validate_option({:alive_continuation_prompt, new}) when is_binary(new), do: :ok
194195
defp validate_option({:width, new}) when is_integer(new), do: :ok
195196
defp validate_option({:parser, tuple}) when tuple_size(tuple) == 3, do: :ok
196197

lib/iex/lib/iex/evaluator.ex

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,21 @@ defmodule IEx.Evaluator do
4949
end
5050
end
5151

52-
# If parsing fails, this might be a TokenMissingError which we treat in
53-
# a special way (to allow for continuation of an expression on the next
54-
# line in IEx).
55-
#
56-
# The first two clauses provide support for the break-trigger allowing to
57-
# break out from a pending incomplete expression. See
58-
# https://github.com/elixir-lang/elixir/issues/1089 for discussion.
59-
@break_trigger "#iex:break\n"
52+
@break_trigger ~c"#iex:break\n"
6053

6154
@op_tokens [:or_op, :and_op, :comp_op, :rel_op, :arrow_op, :in_op] ++
6255
[:three_op, :concat_op, :mult_op]
6356

64-
@doc false
65-
def parse(input, opts, parser_state)
57+
@doc """
58+
Default parsing implementation with support for pipes and #iex:break.
6659
67-
def parse(input, opts, ""), do: parse(input, opts, {"", :other})
60+
If parsing fails, this might be a TokenMissingError which we treat in
61+
a special way (to allow for continuation of an expression on the next
62+
line in IEx).
63+
"""
64+
def parse(input, opts, parser_state)
6865

69-
def parse(@break_trigger, _opts, {"", _} = parser_state) do
70-
{:incomplete, parser_state}
71-
end
66+
def parse(input, opts, []), do: parse(input, opts, {[], :other})
7267

7368
def parse(@break_trigger, opts, _parser_state) do
7469
:elixir_errors.parse_error(
@@ -81,14 +76,13 @@ defmodule IEx.Evaluator do
8176
end
8277

8378
def parse(input, opts, {buffer, last_op}) do
84-
input = buffer <> input
79+
input = buffer ++ input
8580
file = Keyword.get(opts, :file, "nofile")
8681
line = Keyword.get(opts, :line, 1)
8782
column = Keyword.get(opts, :column, 1)
88-
charlist = String.to_charlist(input)
8983

9084
result =
91-
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
85+
with {:ok, tokens} <- :elixir.string_to_tokens(input, line, column, file, opts),
9286
{:ok, adjusted_tokens} <- adjust_operator(tokens, line, column, file, opts, last_op),
9387
{:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
9488
last_op =
@@ -102,7 +96,7 @@ defmodule IEx.Evaluator do
10296

10397
case result do
10498
{:ok, forms, last_op} ->
105-
{:ok, forms, {"", last_op}}
99+
{:ok, forms, {[], last_op}}
106100

107101
{:error, {_, _, ""}} ->
108102
{:incomplete, {input, last_op}}
@@ -113,7 +107,7 @@ defmodule IEx.Evaluator do
113107
file,
114108
error,
115109
token,
116-
{charlist, line, column}
110+
{input, line, column}
117111
)
118112
end
119113
end
@@ -183,9 +177,9 @@ defmodule IEx.Evaluator do
183177

184178
defp loop(%{server: server, ref: ref} = state) do
185179
receive do
186-
{:eval, ^server, code, counter, parser_state} ->
187-
{status, parser_state, state} = parse_eval_inspect(code, counter, parser_state, state)
188-
send(server, {:evaled, self(), status, parser_state})
180+
{:eval, ^server, code, counter} ->
181+
{status, state} = safe_eval_and_inspect(code, counter, state)
182+
send(server, {:evaled, self(), status})
189183
loop(state)
190184

191185
{:fields_from_env, ^server, ref, receiver, fields} ->
@@ -287,32 +281,19 @@ defmodule IEx.Evaluator do
287281
end
288282
end
289283

290-
defp parse_eval_inspect(code, counter, parser_state, state) do
291-
try do
292-
{parser_module, parser_fun, args} = IEx.Config.parser()
293-
args = [code, [line: counter, file: "iex"], parser_state | args]
294-
eval_and_inspect_parsed(apply(parser_module, parser_fun, args), counter, state)
295-
catch
296-
kind, error ->
297-
print_error(kind, error, __STACKTRACE__)
298-
{:error, "", state}
299-
end
300-
end
301-
302-
defp eval_and_inspect_parsed({:ok, forms, parser_state}, counter, state) do
284+
defp safe_eval_and_inspect(forms, counter, state) do
303285
put_history(state)
304286
put_whereami(state)
305-
state = eval_and_inspect(forms, counter, state)
306-
{:ok, parser_state, state}
287+
{:ok, eval_and_inspect(forms, counter, state)}
288+
catch
289+
kind, error ->
290+
print_error(kind, error, __STACKTRACE__)
291+
{:error, state}
307292
after
308293
Process.delete(:iex_history)
309294
Process.delete(:iex_whereami)
310295
end
311296

312-
defp eval_and_inspect_parsed({:incomplete, parser_state}, _counter, state) do
313-
{:incomplete, parser_state, state}
314-
end
315-
316297
defp put_history(%{history: history}) do
317298
Process.put(:iex_history, history)
318299
end
@@ -401,12 +382,7 @@ defmodule IEx.Evaluator do
401382

402383
_ ->
403384
banner = Exception.format_banner(kind, blamed, stacktrace)
404-
405-
if String.contains?(banner, IO.ANSI.reset()) do
406-
[banner]
407-
else
408-
[IEx.color(:eval_error, banner)]
409-
end
385+
[IEx.color(:eval_error, banner)]
410386
end
411387

412388
stackdata = Exception.format_stacktrace(prune_stacktrace(stacktrace))

0 commit comments

Comments
 (0)