diff --git a/lib/livebook/evaluator/io_proxy.ex b/lib/livebook/evaluator/io_proxy.ex index 082775979..10457a38a 100644 --- a/lib/livebook/evaluator/io_proxy.ex +++ b/lib/livebook/evaluator/io_proxy.ex @@ -131,12 +131,12 @@ defmodule Livebook.Evaluator.IOProxy do put_chars(encoding, apply(mod, fun, args), req, state) end - defp io_request({:get_chars, _prompt, count}, state) when count >= 0 do - {{:error, :enotsup}, state} + defp io_request({:get_chars, prompt, count}, state) when count >= 0 do + get_chars(:latin1, prompt, state, count) end - defp io_request({:get_chars, _encoding, _prompt, count}, state) when count >= 0 do - {{:error, :enotsup}, state} + defp io_request({:get_chars, encoding, prompt, count}, state) when count >= 0 do + get_chars(encoding, prompt, state, count) end defp io_request({:get_line, prompt}, state) do @@ -249,6 +249,21 @@ defmodule Livebook.Evaluator.IOProxy do end end + defp get_chars(encoding, prompt, state, count) do + prompt = :unicode.characters_to_binary(prompt, encoding, state.encoding) + + case get_input(prompt, state) do + input when is_binary(input) -> + {chars, rest} = chars_from_input(input, encoding, count) + + state = put_in(state.input_buffers[prompt], rest) + {chars, state} + + error -> + {error, state} + end + end + defp get_input(prompt, state) do Map.get_lazy(state.input_buffers, prompt, fn -> request_input(prompt, state) @@ -289,6 +304,55 @@ defmodule Livebook.Evaluator.IOProxy do end end + defp chars_from_input("", _, _count), do: {:eof, ""} + + defp chars_from_input(input, :unicode, count) do + if byte_size_utf8(input) >= count do + chars_part(input, :unicode, count) + else + {input, ""} + end + end + + defp chars_from_input(input, :latin1, count) do + if byte_size(input) >= count do + chars_part(input, :latin1, count) + else + {input, ""} + end + end + + defp chars_part(chars, _, 0), do: {"", chars} + + defp chars_part(input, :unicode, count) do + with {:ok, count} <- split_at(input, count, 0) do + <> = input + {chars, rest} + end + end + + defp chars_part(input, :latin1, count) do + <> = input + {chars, rest} + end + + defp split_at(_, 0, acc), do: {:ok, acc} + + defp split_at(<>, count, acc), + do: split_at(t, count - 1, acc + byte_size(<>)) + + defp split_at(<<_, _::binary>>, _count, _acc), + do: {:error, :invalid_unicode} + + defp split_at(<<>>, _count, acc), + do: {:ok, acc} + + defp byte_size_utf8(chars), do: byte_size_utf8(chars, 0) + + defp byte_size_utf8(<<>>, size), do: size + + defp byte_size_utf8(<<_h::utf8, t::binary>>, size), do: byte_size_utf8(t, size + 1) + defp io_reply(from, reply_as, reply) do send(from, {:io_reply, reply_as, reply}) end diff --git a/lib/livebook/notebook/cell/input.ex b/lib/livebook/notebook/cell/input.ex index bf12539a3..86912a84c 100644 --- a/lib/livebook/notebook/cell/input.ex +++ b/lib/livebook/notebook/cell/input.ex @@ -20,7 +20,7 @@ defmodule Livebook.Notebook.Cell.Input do reactive: boolean() } - @type type :: :text | :url | :number | :password + @type type :: :text | :url | :number | :password | :textarea @doc """ Returns an empty cell. @@ -50,6 +50,8 @@ defmodule Livebook.Notebook.Cell.Input do defp validate_value(_value, :password), do: :ok + defp validate_value(_value, :textarea), do: :ok + defp validate_value(value, :url) do if Utils.valid_url?(value) do :ok diff --git a/lib/livebook_web/live/session_live/cell_component.ex b/lib/livebook_web/live/session_live/cell_component.ex index 0c0ef643d..0641e7b74 100644 --- a/lib/livebook_web/live/session_live/cell_component.ex +++ b/lib/livebook_web/live/session_live/cell_component.ex @@ -191,15 +191,26 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<%= @cell_view.name %>
- " - data-element="input" - class="input <%= if(@cell_view.error, do: "input--error") %>" - name="value" - value="<%= @cell_view.value %>" - phx-debounce="300" - spellcheck="false" - autocomplete="off" - tabindex="-1" /> + + <%= if (@cell_view.input_type == :textarea) do %> + + <% else %> + " + data-element="input" + class="input <%= if(@cell_view.error, do: "input--error") %>" + name="value" + value="<%= @cell_view.value %>" + phx-debounce="300" + spellcheck="false" + autocomplete="off" + tabindex="-1" /> + <% end %> + <%= if @cell_view.error do %>
<%= String.capitalize(@cell_view.error) %> diff --git a/lib/livebook_web/live/session_live/input_cell_settings_component.ex b/lib/livebook_web/live/session_live/input_cell_settings_component.ex index 70e2118d7..c629c2c2c 100644 --- a/lib/livebook_web/live/session_live/input_cell_settings_component.ex +++ b/lib/livebook_web/live/session_live/input_cell_settings_component.ex @@ -28,7 +28,7 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
Type
- <%= render_select("type", [number: "Number", password: "Password", text: "Text", url: "URL"], @type) %> + <%= render_select("type", [number: "Number", password: "Password", text: "Text", textarea: "Textarea", url: "URL"], @type) %>
Name
diff --git a/test/livebook/evaluator/io_proxy_test.exs b/test/livebook/evaluator/io_proxy_test.exs index 714b54a74..11e6db612 100644 --- a/test/livebook/evaluator/io_proxy_test.exs +++ b/test/livebook/evaluator/io_proxy_test.exs @@ -121,6 +121,41 @@ defmodule Livebook.Evaluator.IOProxyTest do assert IOProxy.flush_widgets(io) == MapSet.new() end + test "getn/1 return first character", %{io: io} do + pid = + spawn_link(fn -> + reply_to_input_request(:ref, "name: ", {:ok, "🐈 test\n"}, 1) + end) + + IOProxy.configure(io, pid, :ref) + + assert IO.getn(io, "name: ") == "🐈" + end + + test "getn/2 returns the number of defined characters ", %{io: io} do + pid = + spawn_link(fn -> + reply_to_input_request(:ref, "name: ", {:ok, "Jake Peralta\nAmy Santiago\n"}, 1) + end) + + IOProxy.configure(io, pid, :ref) + + assert IO.getn(io, "name: ", 13) == "Jake Peralta\n" + assert IO.getn(io, "name: ", 13) == "Amy Santiago\n" + assert IO.getn(io, "name: ", 13) == :eof + end + + test "getn/2 all characters", %{io: io} do + pid = + spawn_link(fn -> + reply_to_input_request(:ref, "name: ", {:ok, "Jake Peralta\nAmy Santiago\n"}, 1) + end) + + IOProxy.configure(io, pid, :ref) + + assert IO.getn(io, "name: ", 10_000) == "Jake Peralta\nAmy Santiago\n" + end + # Helpers defp reply_to_input_request(_ref, _prompt, _reply, 0), do: :ok