diff --git a/assets/css/components.css b/assets/css/components.css index 05c952664..d653390db 100644 --- a/assets/css/components.css +++ b/assets/css/components.css @@ -36,6 +36,14 @@ @apply px-2 py-1 bg-gray-50 border-gray-200 text-gray-600 hover:bg-gray-100 focus:bg-gray-100; } +.button-square-icon { + @apply p-0 flex items-center justify-center h-10 w-10; +} + +.button-square-icon i { + @apply text-xl leading-none; +} + .choice-button { @apply px-5 py-2 rounded-lg border text-gray-700 bg-white border-gray-200; } diff --git a/assets/css/tooltips.css b/assets/css/tooltips.css index 77e69a1e4..5631dd01d 100644 --- a/assets/css/tooltips.css +++ b/assets/css/tooltips.css @@ -55,14 +55,12 @@ Example usage: transition-delay: 0s; } -.tooltip:hover:before, -.tooltip:focus-within:before { +.tooltip:hover:before { visibility: visible; transition-delay: var(--show-delay); } -.tooltip:hover:after, -.tooltip:focus-within:after { +.tooltip:hover:after { visibility: visible; transition-delay: var(--show-delay); } diff --git a/lib/livebook/evaluator/string_formatter.ex b/lib/livebook/evaluator/string_formatter.ex index 8a6bce9f3..ce57cd9fd 100644 --- a/lib/livebook/evaluator/string_formatter.ex +++ b/lib/livebook/evaluator/string_formatter.ex @@ -13,8 +13,13 @@ defmodule Livebook.Evaluator.StringFormatter do :ignored end + def format({:ok, {:module, _, _, _} = value}) do + inspected = inspect(value, inspect_opts(limit: 10)) + {:inspect, inspected} + end + def format({:ok, value}) do - inspected = inspect(value, pretty: true, width: 100, syntax_colors: syntax_colors()) + inspected = inspect(value, inspect_opts()) {:inspect, inspected} end @@ -23,6 +28,11 @@ defmodule Livebook.Evaluator.StringFormatter do {:error, formatted} end + defp inspect_opts(opts \\ []) do + default_opts = [pretty: true, width: 100, syntax_colors: syntax_colors()] + Keyword.merge(default_opts, opts) + end + defp syntax_colors() do # Note: we intentionally don't specify colors # for `:binary`, `:list`, `:map` and `:tuple` diff --git a/lib/livebook/notebook/hello_livebook.ex b/lib/livebook/notebook/hello_livebook.ex new file mode 100644 index 000000000..dda3bf6f3 --- /dev/null +++ b/lib/livebook/notebook/hello_livebook.ex @@ -0,0 +1,191 @@ +defmodule Livebook.Notebook.HelloLivebook do + livemd = ~s''' + # Hello Livebook + + ## Introduction + + We are happy you decided to give Livebook a try, hopefully it empowers + you to build great stuff 🚀 + + Livebook is a tool for crafting **interactive** and **collaborative** code notebooks. + It is primarily meant as a tool rapid prototyping - think of it as an IEx session + combined with your editor. + You can also use it for authoring shareable articles that people can easily + run and play around with. + Package authors can write notebooks as interactive tutorials + and include them in their repository, so that users can easily download + and run them locally. + + ## Basic usage + + Each notebook consists of a number of cells, which serve as primary building blocks. + There are **Markdown** cells (such as this one) that allow you to describe your work + and **Elixir** cells where the magic takes place! + + To insert a new cell move your between cells and click one of the revealed buttons. 👇 + + ```elixir + # This is an Elixir cell - as the name suggests that's where the code goes. + # To evaluate this cell, you can either press the "Evaluate" button above + # or use `ctrl + enter` like a boss! + + message = "hey, grab yourself a cup of 🍵" + ``` + + Subsequent cells have access to the bindings you defined! + + ```elixir + String.replace(message, "🍵", "☕") + ``` + + Note however that bindings are not global, so each cell *sees* only stuff that goes + above itself. This approach helps to keep the notebook clean and predictable + as you keep working on it! + + ## Sections + + You can leverage so called **sections** to nicely group related cells together. + Click on the book icon in the left sidebar to reveal a list of all sections. + As you can see, this approach helps to easily jump around the notebook, + especially once it gets grows. + + Let's make use of this section to see how output is captured! + + ```elixir + cats = ~w(😼 😹 😻 😺 😸 😽) + + for _ <- 1..3 do + cats + |> Enum.take_random(3) + |> Enum.join(" ") + |> IO.puts() + end + ``` + + ## Notebook files + + My default notebooks are kept in memory, which is fine for interactive hacking, + but oftentimes you will want to save your work for later. Fortunately notebooks + can be persisted by clicking on the "Settings" icon in the left sidebar + and selecting the file location. + + Notebooks are stored in **live markdown** format, which is essentially the markdown you know, + with just a few assumptions on how particular elements are represented. Thanks to this + approach you can easily keep notebooks under version control and get readable diffs. + You can also easily preview those files, reuse for blog posts and even edit in a text editor. + + ## Modules + + As we already saw, Elixir cells can be used for working on tiny snippets, + but you may as well define a module! + + ```elixir + defmodule Utils do + @doc """ + Generates a random binary id. + """ + @spec random_id() :: binary() + def random_id() do + :crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower) + end + end + ``` + + If you're surprised by the above output, keep in mind that + every Elixir expression evaluates to some value and as so does module compilation! + + Having the module defined, let's take it for a spin. + + ```elixir + Utils.random_id() + ``` + + ## Imports + + You can import modules as normally to make the imported functions visible + to all subsequent cells. Usually you want to keep `import`, `alias` and `require` + in the first section, as part of the notebook setup. + + ```elixir + import IEx.Helpers + ``` + + ```elixir + h(Enum.map()) + ``` + + ```elixir + # Sidenote: http://www.numbat.org.au/thenumbat + i("I ❤️ Numbats") + ``` + + ## Runtimes + + Livebook has a concept of **runtime**, which in practice is an Elixir node responsible + for evaluating your code. + + By default a new Elixir node is started (similarly to starting `iex`), + but you can also choose to run inside a Mix project (as you would with `iex -S mix`) + or even manually attach to an existing distributed node! + You can configure the runtime under "Notebook settings". + + ## Using packages + + Sometimes you need a dependency or two and notebooks are no exception to this. + + One way to work with packages is to create a Mix project and configure the notebook + to run its context (as pointed out above). This approach makes sense if you already have + a Mix project you are working on, especially because this makes all project's + modules available as well. + + But there are cases when you just want to play around with a new package + or quickly prototype some code that relies on such. Fortunately, starting + version `v1.12` Elixir ships with `Mix.install/2` that allows for installing + dependencies into Elixir runtime! This approach is especially useful for sharing notebooks, + because everyone will be able to get the same dependencies. Let's try this out: + + ```elixir + # Note: this requires Elixir version >= 1.12 + Mix.install([ + {:jason, "~> 1.2"} + ]) + ``` + + ```elixir + %{elixir: "rulez"} + |> Jason.encode!() + |> IO.puts() + ``` + + It is a good idea to specify versions of the installed packages, + so that the notebook is easily reproducible later on. + + Also keep in mind that `Mix.install/2` can be called only once + per runtime, so if you need to modify the dependencies, you should + go to the notebook runtime configuration and **reconnect** the current runtime. + + ## Stepping up your workflow + + Once you start using notebooks more, it's gonna be beneficial + to optimise how you move around. Livebook leverages the concept of + **navigation**/**insert** modes and offers many shortcuts for common operations. + Make sure to check out the shortcuts by clicking the "Keyboard" icon in + the sidebar panel or by typing `?`. + + ## Final notes + + Livebook is an open source project, so feel free to look into + [the repository](https://github.com/elixir-nx/livebook) + to contribute, report bugs, suggest features or just skim over the codebase. + + Now go ahead and build something cool! 🚢 + ''' + + {notebook, []} = Livebook.LiveMarkdown.Import.notebook_from_markdown(livemd) + + @notebook notebook + + def new() do + @notebook + end +end diff --git a/lib/livebook_web/live/home_live.ex b/lib/livebook_web/live/home_live.ex index e31184d8d..e2c138b08 100644 --- a/lib/livebook_web/live/home_live.ex +++ b/lib/livebook_web/live/home_live.ex @@ -36,10 +36,18 @@ defmodule LivebookWeb.HomeLive do
Livebook
- +
+ + + + +
<%= live_component @socket, LivebookWeb.PathSelectComponent, @@ -108,6 +116,10 @@ defmodule LivebookWeb.HomeLive do {:noreply, assign(socket, path: path)} end + def handle_event("hello_livebook", %{}, socket) do + create_session(socket, notebook: Livebook.Notebook.HelloLivebook.new()) + end + def handle_event("new", %{}, socket) do create_session(socket) end diff --git a/mix.exs b/mix.exs index ffdbe6b26..bb255b906 100644 --- a/mix.exs +++ b/mix.exs @@ -36,7 +36,9 @@ defmodule Livebook.MixProject do defp deps do [ {:phoenix, "~> 1.5.7"}, - {:phoenix_live_view, "~> 0.15.0"}, + # TODO: remove reference to the Git repo once LV 0.15.5 is released + {:phoenix_live_view, "~> 0.15.0", + github: "phoenixframework/phoenix_live_view", branch: "master"}, {:floki, ">= 0.27.0", only: :test}, {:phoenix_html, "~> 2.11"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, diff --git a/mix.lock b/mix.lock index 9c25504ed..33e67c847 100644 --- a/mix.lock +++ b/mix.lock @@ -8,14 +8,14 @@ "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, - "phoenix": {:hex, :phoenix, "1.5.7", "2923bb3af924f184459fe4fa4b100bd25fa6468e69b2803dfae82698269aa5e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "774cd64417c5a3788414fdbb2be2eb9bcd0c048d9e6ad11a0c1fd67b7c0d0978"}, + "phoenix": {:hex, :phoenix, "1.5.8", "71cfa7a9bb9a37af4df98939790642f210e35f696b935ca6d9d9c55a884621a4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35ded0a32f4836168c7ab6c33b88822eccd201bcd9492125a9bea4c54332d955"}, "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.15.4", "86908dc9603cc81c07e84725ee42349b5325cb250c9c20d3533856ff18dbb7dc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35d78f3c35fe10a995dca5f4ab50165b7a90cbe02e23de245381558f821e9462"}, + "phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "ffa5d555368f2ca42d57bc573bba63e01c218226", [branch: "master"]}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, - "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, + "plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"}, "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.1", "5c854427528bf61d159855cedddffc0625e2228b5f30eff76d5a4de42d896ef4", [:mix], [], "hexpm", "6961c0e17febd9d0bfa89632d391d2545d2e0eb73768f5f50305a23961d8782c"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"}, diff --git a/test/livebook/notebook/hello_livebook_test.exs b/test/livebook/notebook/hello_livebook_test.exs new file mode 100644 index 000000000..6dc61d2cd --- /dev/null +++ b/test/livebook/notebook/hello_livebook_test.exs @@ -0,0 +1,10 @@ +defmodule Livebook.Notebook.HelloLivebookTest do + use ExUnit.Case, async: true + + alias Livebook.Notebook + alias Livebook.Notebook.HelloLivebook + + test "new/0 correctly builds a new notebook" do + assert %Notebook{} = HelloLivebook.new() + end +end diff --git a/test/livebook_web/live/home_live_test.exs b/test/livebook_web/live/home_live_test.exs index baa869f4a..7c813ce58 100644 --- a/test/livebook_web/live/home_live_test.exs +++ b/test/livebook_web/live/home_live_test.exs @@ -136,6 +136,20 @@ defmodule LivebookWeb.HomeLiveTest do end end + test "link to introductory notebook correctly creates a new session", %{conn: conn} do + {:ok, view, _} = live(conn, "/") + + assert {:error, {:live_redirect, %{to: to}}} = + view + |> element(~s{[aria-label="Introduction"] button}) + |> render_click() + + assert to =~ "/sessions/" + + {:ok, view, _} = live(conn, to) + assert render(view) =~ "Hello Livebook" + end + # Helpers defp test_notebook_path(name) do