diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex index 3fac67f0000..046d648a416 100644 --- a/lib/iex/lib/iex.ex +++ b/lib/iex/lib/iex.ex @@ -396,10 +396,8 @@ defmodule IEx do The supported options are: * `:auto_reload` - * `:alive_continuation_prompt` * `:alive_prompt` * `:colors` - * `:continuation_prompt` * `:default_prompt` * `:dot_iex` * `:history_size` @@ -485,14 +483,8 @@ defmodule IEx do * `:default_prompt` - used when `Node.alive?/0` returns `false` - * `:continuation_prompt` - used when `Node.alive?/0` returns `false` - and more input is expected - * `:alive_prompt` - used when `Node.alive?/0` returns `true` - * `:alive_continuation_prompt` - used when `Node.alive?/0` returns - `true` and more input is expected - The following values in the prompt string will be replaced appropriately: * `%counter` - the index of the history @@ -506,11 +498,17 @@ defmodule IEx do The parser is a "mfargs", which is a tuple with three elements: the module name, the function name, and extra arguments to be appended. The parser receives at least three arguments, the - current input as a string, the parsing options as a keyword list, - and the buffer as a string. It must return `{:ok, expr, buffer}` - or `{:incomplete, buffer}`. + current input as a charlist, the parsing options as a keyword list, + and the state. The initial state is an empty charlist. It must + return `{:ok, expr, state}` or `{:incomplete, state}`. + + If the parser raises, the state is reset to an empty charlist. - If the parser raises, the buffer is reset to an empty string. + > In earlier Elixir versions, the parser would receive the input + > and the initial buffer as strings. However, this behaviour + > changed when Erlang/OTP introduced multiline editing. If you + > support earlier Elixir versions, you can normalize the inputs + > by calling `to_charlist/1`. ## `.iex` diff --git a/lib/iex/lib/iex/app.ex b/lib/iex/lib/iex/app.ex index 8b25f19daaf..8ab2ccb2418 100644 --- a/lib/iex/lib/iex/app.ex +++ b/lib/iex/lib/iex/app.ex @@ -8,6 +8,10 @@ defmodule IEx.App do use Application def start(_type, _args) do + with :default <- Application.get_env(:stdlib, :shell_multiline_prompt, :default) do + Application.put_env(:stdlib, :shell_multiline_prompt, {IEx.Config, :prompt}) + end + children = [IEx.Config, IEx.Broker, IEx.Pry] Supervisor.start_link(children, strategy: :one_for_one, name: IEx.Supervisor) end diff --git a/lib/iex/lib/iex/config.ex b/lib/iex/lib/iex/config.ex index ddb3fe670d7..b9d3f148981 100644 --- a/lib/iex/lib/iex/config.ex +++ b/lib/iex/lib/iex/config.ex @@ -13,15 +13,37 @@ defmodule IEx.Config do :inspect, :history_size, :default_prompt, - :continuation_prompt, :alive_prompt, - :alive_continuation_prompt, :width, :parser, :dot_iex, :auto_reload ] + # Generate a continuation prompt based on IEx prompt. + # This is set as global configuration on app start. + def prompt(prompt) do + case Enum.split_while(prompt, &(&1 != ?()) do + # It is not the default Elixir shell, so we use the default prompt + {_, []} -> + List.duplicate(?\s, max(0, prompt_width(prompt) - 3)) ++ ~c".. " + + {left, right} -> + List.duplicate(?., prompt_width(left)) ++ right + end + end + + # TODO: Remove this when we require Erlang/OTP 27+ + @compile {:no_warn_undefined, :prim_tty} + @compile {:no_warn_undefined, :shell} + defp prompt_width(prompt) do + if function_exported?(:prim_tty, :npwcwidthstring, 1) do + :prim_tty.npwcwidthstring(prompt) + else + :shell.prompt_width(prompt) + end + end + # Read API def configuration() do @@ -53,20 +75,12 @@ defmodule IEx.Config do Application.fetch_env!(:iex, :default_prompt) end - def continuation_prompt() do - Application.get_env(:iex, :continuation_prompt, default_prompt()) - end - def alive_prompt() do Application.fetch_env!(:iex, :alive_prompt) end - def alive_continuation_prompt() do - Application.get_env(:iex, :alive_continuation_prompt, alive_prompt()) - end - def parser() do - Application.get_env(:iex, :parser, {IEx.Evaluator, :parse, []}) + Application.fetch_env!(:iex, :parser) end def color(color) do @@ -202,9 +216,7 @@ defmodule IEx.Config do defp validate_option({:inspect, new}) when is_list(new), do: :ok defp validate_option({:history_size, new}) when is_integer(new), do: :ok defp validate_option({:default_prompt, new}) when is_binary(new), do: :ok - defp validate_option({:continuation_prompt, new}) when is_binary(new), do: :ok defp validate_option({:alive_prompt, new}) when is_binary(new), do: :ok - defp validate_option({:alive_continuation_prompt, new}) when is_binary(new), do: :ok defp validate_option({:width, new}) when is_integer(new), do: :ok defp validate_option({:parser, tuple}) when tuple_size(tuple) == 3, do: :ok defp validate_option({:dot_iex, path}) when is_binary(path), do: :ok diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 4484dc87250..9c04e1a5281 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -55,26 +55,21 @@ defmodule IEx.Evaluator do end end - # If parsing fails, this might be a TokenMissingError which we treat in - # a special way (to allow for continuation of an expression on the next - # line in IEx). - # - # The first two clauses provide support for the break-trigger allowing to - # break out from a pending incomplete expression. See - # https://github.com/elixir-lang/elixir/issues/1089 for discussion. - @break_trigger "#iex:break\n" + @break_trigger ~c"#iex:break\n" @op_tokens [:or_op, :and_op, :comp_op, :rel_op, :arrow_op, :in_op] ++ [:three_op, :concat_op, :mult_op] - @doc false - def parse(input, opts, parser_state) + @doc """ + Default parsing implementation with support for pipes and #iex:break. - def parse(input, opts, ""), do: parse(input, opts, {"", :other}) + If parsing fails, this might be a TokenMissingError which we treat in + a special way (to allow for continuation of an expression on the next + line in IEx). + """ + def parse(input, opts, parser_state) - def parse(@break_trigger, _opts, {"", _} = parser_state) do - {:incomplete, parser_state} - end + def parse(input, opts, []), do: parse(input, opts, {[], :other}) def parse(@break_trigger, opts, _parser_state) do :elixir_errors.parse_error( @@ -87,14 +82,13 @@ defmodule IEx.Evaluator do end def parse(input, opts, {buffer, last_op}) do - input = buffer <> input + input = buffer ++ input file = Keyword.get(opts, :file, "nofile") line = Keyword.get(opts, :line, 1) column = Keyword.get(opts, :column, 1) - charlist = String.to_charlist(input) result = - with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts), + with {:ok, tokens} <- :elixir.string_to_tokens(input, line, column, file, opts), {:ok, adjusted_tokens} <- adjust_operator(tokens, line, column, file, opts, last_op), {:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do last_op = @@ -108,7 +102,7 @@ defmodule IEx.Evaluator do case result do {:ok, forms, last_op} -> - {:ok, forms, {"", last_op}} + {:ok, forms, {[], last_op}} {:error, {_, _, ""}} -> {:incomplete, {input, last_op}} @@ -119,7 +113,7 @@ defmodule IEx.Evaluator do file, error, token, - {charlist, line, column, 0} + {input, line, column, 0} ) end end @@ -189,9 +183,9 @@ defmodule IEx.Evaluator do defp loop(%{server: server, ref: ref} = state) do receive do - {:eval, ^server, code, counter, parser_state} -> - {status, parser_state, state} = parse_eval_inspect(code, counter, parser_state, state) - send(server, {:evaled, self(), status, parser_state}) + {:eval, ^server, code, counter} -> + {status, state} = safe_eval_and_inspect(code, counter, state) + send(server, {:evaled, self(), status}) loop(state) {:fields_from_env, ^server, ref, receiver, fields} -> @@ -296,32 +290,19 @@ defmodule IEx.Evaluator do end end - defp parse_eval_inspect(code, counter, parser_state, state) do - try do - {parser_module, parser_fun, args} = IEx.Config.parser() - args = [code, [line: counter, file: "iex"], parser_state | args] - eval_and_inspect_parsed(apply(parser_module, parser_fun, args), counter, state) - catch - kind, error -> - print_error(kind, error, __STACKTRACE__) - {:error, "", state} - end - end - - defp eval_and_inspect_parsed({:ok, forms, parser_state}, counter, state) do + defp safe_eval_and_inspect(forms, counter, state) do put_history(state) put_whereami(state) - state = eval_and_inspect(forms, counter, state) - {:ok, parser_state, state} + {:ok, eval_and_inspect(forms, counter, state)} + catch + kind, error -> + print_error(kind, error, __STACKTRACE__) + {:error, state} after Process.delete(:iex_history) Process.delete(:iex_whereami) end - defp eval_and_inspect_parsed({:incomplete, parser_state}, _counter, state) do - {:incomplete, parser_state, state} - end - defp put_history(%{history: history}) do Process.put(:iex_history, history) end @@ -410,12 +391,7 @@ defmodule IEx.Evaluator do _ -> banner = Exception.format_banner(kind, blamed, stacktrace) - - if String.contains?(banner, IO.ANSI.reset()) do - [banner] - else - [IEx.color(:eval_error, banner)] - end + [IEx.color(:eval_error, banner)] end stackdata = Exception.format_stacktrace(prune_stacktrace(stacktrace)) diff --git a/lib/iex/lib/iex/server.ex b/lib/iex/lib/iex/server.ex index ef33905d5dc..5ab6d8eff04 100644 --- a/lib/iex/lib/iex/server.ex +++ b/lib/iex/lib/iex/server.ex @@ -15,7 +15,7 @@ defmodule IEx.Server do """ @doc false - defstruct parser_state: "", + defstruct parser_state: [], counter: 1, prefix: "iex", on_eof: :stop_evaluator, @@ -92,7 +92,7 @@ defmodule IEx.Server do ) evaluator = start_evaluator(state.counter, Keyword.merge(state.evaluator_options, opts)) - loop(state, :ok, evaluator, Process.monitor(evaluator), input) + loop(state, evaluator, Process.monitor(evaluator), input) end # Starts an evaluator using the provided options. @@ -121,18 +121,19 @@ defmodule IEx.Server do run_without_registration(state, opts, input) end - defp loop(state, status, evaluator, evaluator_ref, input) do - :io.setopts(expand_fun: state.expand_fun) - input = input || io_get(prompt(status, state.prefix, state.counter)) + defp loop(state, evaluator, evaluator_ref, input) do + %{counter: counter, expand_fun: expand_fun, prefix: prefix, parser_state: parser} = state + :io.setopts(expand_fun: expand_fun) + input = input || io_get(prompt(prefix, counter), counter, parser) wait_input(state, evaluator, evaluator_ref, input) end defp wait_input(state, evaluator, evaluator_ref, input) do receive do - {:io_reply, ^input, code} when is_binary(code) -> + {:io_reply, ^input, {:ok, code, parser_state}} -> :io.setopts(expand_fun: fn _ -> {:yes, [], []} end) - send(evaluator, {:eval, self(), code, state.counter, state.parser_state}) - wait_eval(state, evaluator, evaluator_ref) + send(evaluator, {:eval, self(), code, state.counter}) + wait_eval(%{state | parser_state: parser_state}, evaluator, evaluator_ref) {:io_reply, ^input, :eof} -> case state.on_eof do @@ -140,15 +141,29 @@ defmodule IEx.Server do :stop_evaluator -> stop_evaluator(evaluator, evaluator_ref) end + {:io_reply, ^input, {:error, kind, error, stacktrace}} -> + banner = Exception.format_banner(kind, error, stacktrace) + + banner = + if String.contains?(banner, IO.ANSI.reset()) do + banner + else + IEx.color(:eval_error, banner) + end + + stackdata = Exception.format_stacktrace(stacktrace) + IO.write(:stdio, [banner, ?\n, IEx.color(:stack_info, stackdata)]) + loop(%{state | parser_state: []}, evaluator, evaluator_ref, nil) + # Triggered by pressing "i" as the job control switch {:io_reply, ^input, {:error, :interrupted}} -> io_error("** (EXIT) interrupted") - loop(%{state | parser_state: ""}, :ok, evaluator, evaluator_ref, nil) + loop(%{state | parser_state: []}, evaluator, evaluator_ref, nil) # Unknown IO message {:io_reply, ^input, msg} -> io_error("** (EXIT) unknown IO message: #{inspect(msg)}") - loop(%{state | parser_state: ""}, :ok, evaluator, evaluator_ref, nil) + loop(%{state | parser_state: []}, evaluator, evaluator_ref, nil) # Triggered when IO dies while waiting for input {:DOWN, ^input, _, _, _} -> @@ -163,10 +178,10 @@ defmodule IEx.Server do defp wait_eval(state, evaluator, evaluator_ref) do receive do - {:evaled, ^evaluator, status, parser_state} -> + {:evaled, ^evaluator, status} -> counter = if(status == :ok, do: state.counter + 1, else: state.counter) - state = %{state | counter: counter, parser_state: parser_state} - loop(state, status, evaluator, evaluator_ref, nil) + state = %{state | counter: counter} + loop(state, evaluator, evaluator_ref, nil) msg -> handle_common(msg, state, evaluator, evaluator_ref, nil, fn state -> @@ -203,7 +218,7 @@ defmodule IEx.Server do if take_over?(take_pid, take_ref, state.counter + 1, true) do # Since we are in process, also bump the counter state = reset_state(bump_counter(state)) - loop(state, :ok, evaluator, evaluator_ref, input) + loop(state, evaluator, evaluator_ref, input) else callback.(state) end @@ -346,7 +361,7 @@ defmodule IEx.Server do # Once the rerunning session restarts, we keep the same evaluator_options # and rollback to a new evaluator. defp reset_state(state) do - %{state | parser_state: ""} + %{state | parser_state: []} end defp bump_counter(state) do @@ -355,28 +370,41 @@ defmodule IEx.Server do ## IO - defp io_get(prompt) do + defp io_get(prompt, counter, parser_state) do gl = Process.group_leader() ref = Process.monitor(gl) - command = {:get_until, :unicode, prompt, __MODULE__, :__parse__, []} + command = {:get_until, :unicode, prompt, __MODULE__, :__parse__, [{counter, parser_state}]} send(gl, {:io_request, self(), ref, command}) ref end @doc false - def __parse__([], :eof), do: {:done, :eof, []} - def __parse__([], chars), do: {:done, List.to_string(chars), []} + def __parse__(_, :eof, _parser_state), do: {:done, :eof, []} + + def __parse__([], chars, {counter, parser_state} = to_be_unused) do + __parse__({counter, parser_state, IEx.Config.parser()}, chars, to_be_unused) + end + + def __parse__({counter, parser_state, mfa}, chars, _unused) do + {parser_module, parser_fun, args} = mfa + args = [chars, [line: counter, file: "iex"], parser_state | args] + + case apply(parser_module, parser_fun, args) do + {:ok, forms, parser_state} -> {:done, {:ok, forms, parser_state}, []} + {:incomplete, parser_state} -> {:more, {counter, parser_state, mfa}} + end + catch + kind, error -> + {:done, {:error, kind, error, __STACKTRACE__}, []} + end - defp prompt(status, prefix, counter) do - {mode, prefix} = + defp prompt(prefix, counter) do + prompt = if Node.alive?() do - {prompt_mode(status, :alive), default_prefix(status, prefix)} + IEx.Config.alive_prompt() else - {prompt_mode(status, :default), default_prefix(status, prefix)} + IEx.Config.default_prompt() end - - prompt = - apply(IEx.Config, mode, []) |> String.replace("%counter", to_string(counter)) |> String.replace("%prefix", to_string(prefix)) |> String.replace("%node", to_string(node())) @@ -384,14 +412,6 @@ defmodule IEx.Server do [prompt, " "] end - defp default_prefix(:incomplete, _prefix), do: "..." - defp default_prefix(_ok_or_error, prefix), do: prefix - - defp prompt_mode(:incomplete, :default), do: :continuation_prompt - defp prompt_mode(:incomplete, :alive), do: :alive_continuation_prompt - defp prompt_mode(_ok_or_error, :default), do: :default_prompt - defp prompt_mode(_ok_or_error, :alive), do: :alive_prompt - defp io_error(result) do IO.puts(:stdio, IEx.color(:eval_error, result)) end diff --git a/lib/iex/mix.exs b/lib/iex/mix.exs index 4867ba3ddf3..edd1b709218 100644 --- a/lib/iex/mix.exs +++ b/lib/iex/mix.exs @@ -19,6 +19,7 @@ defmodule IEx.MixProject do mod: {IEx.App, []}, env: [ colors: [], + parser: {IEx.Evaluator, :parse, []}, inspect: [pretty: true], history_size: 20, default_prompt: "%prefix(%counter)>", diff --git a/lib/iex/test/iex/autocomplete_test.exs b/lib/iex/test/iex/autocomplete_test.exs index 2323191b0b8..99b526f7efa 100644 --- a/lib/iex/test/iex/autocomplete_test.exs +++ b/lib/iex/test/iex/autocomplete_test.exs @@ -17,8 +17,8 @@ defmodule IEx.AutocompleteTest do ExUnit.CaptureIO.capture_io(fn -> evaluator = Process.get(:evaluator) Process.group_leader(evaluator, Process.group_leader()) - send(evaluator, {:eval, self(), line <> "\n", 1, ""}) - assert_receive {:evaled, _, _, _} + send(evaluator, {:eval, self(), Code.string_to_quoted!(line <> "\n"), 1}) + assert_receive {:evaled, _, _} end) end diff --git a/lib/iex/test/iex/config_test.exs b/lib/iex/test/iex/config_test.exs new file mode 100644 index 00000000000..f157caa8ea2 --- /dev/null +++ b/lib/iex/test/iex/config_test.exs @@ -0,0 +1,23 @@ +Code.require_file("../test_helper.exs", __DIR__) + +defmodule IEx.ConfigTest do + use ExUnit.Case, async: true + + import IEx.Config + + describe "prompt" do + test "converts everything before opening parens to dots" do + assert prompt(~c"iex(321)>") == ~c"...(321)>" + assert prompt(~c"foo-bar(321)>") == ~c".......(321)>" + end + + test "falls back to Erlang wit no parens around" do + assert prompt(~c"foo-bar>") == ~c" .. " + end + + test "ignores ansi escapes" do + assert prompt(~c"#{IO.ANSI.red()}iex(foo)>") == ~c"...(foo)>" + assert prompt(~c"#{IO.ANSI.red()}foo-bar>") == ~c" .. " + end + end +end diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs index 247281a580e..e6c5cd3aa50 100644 --- a/lib/iex/test/iex/interaction_test.exs +++ b/lib/iex/test/iex/interaction_test.exs @@ -145,11 +145,6 @@ defmodule IEx.InteractionTest do assert capture_iex("1\n", opts, [], true) == "prompt(1)> 1\nprompt(2)>" end - test "continuation prompt" do - opts = [default_prompt: "%prefix(%counter)>", continuation_prompt: "%prefix(%counter)>>>"] - assert capture_iex("[\n1\n]\n", opts, [], true) == "iex(1)> ...(1)>>> ...(1)>>> [1]\niex(2)>" - end - if IO.ANSI.enabled?() do test "color" do opts = [colors: [enabled: true, eval_result: [:red]]] @@ -266,7 +261,7 @@ defmodule IEx.InteractionTest do end end - assert capture_iex("foo", parser: {EchoParser, :parse, []}) == "\"foo\"" + assert capture_iex("foo", parser: {EchoParser, :parse, []}) == "~c\"foo\"" after IEx.configure(parser: {IEx.Evaluator, :parse, []}) end