Start new notebooks with a focused code cell (#527)

* Start new notebooks with a focused code cell

* Update changelog
This commit is contained in:
Jonatan Kłosko 2021-08-30 16:33:40 +02:00 committed by GitHub
parent 806101aa25
commit 2b1cb57a15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 71 additions and 26 deletions

View file

@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- Improved intellisense to handle structs and sigils ([#513](https://github.com/livebook-dev/livebook/pull/513)) - Improved intellisense to handle structs and sigils ([#513](https://github.com/livebook-dev/livebook/pull/513))
- Create new notebooks with an already focused code cell ([#527](https://github.com/livebook-dev/livebook/pull/527))
### Fixed ### Fixed

View file

@ -5,6 +5,7 @@ import {
selectElementContent, selectElementContent,
smoothlyScrollToElement, smoothlyScrollToElement,
} from "../lib/utils"; } from "../lib/utils";
import { getAttributeOrDefault } from "../lib/attribute";
import KeyBuffer from "./key_buffer"; import KeyBuffer from "./key_buffer";
import { globalPubSub } from "../lib/pub_sub"; import { globalPubSub } from "../lib/pub_sub";
import monaco from "../cell/live_editor/monaco"; import monaco from "../cell/live_editor/monaco";
@ -18,6 +19,11 @@ import monaco from "../cell/live_editor/monaco";
* to communicate between this global hook and cells and for * to communicate between this global hook and cells and for
* that we use a simple local pubsub that the hooks subscribe to. * that we use a simple local pubsub that the hooks subscribe to.
* *
* Configuration:
*
* * `data-autofocus-cell-id` - id of the cell that gets initial
* focus once the notebook is loaded
*
* ## Shortcuts * ## Shortcuts
* *
* This hook registers session shortcut handlers, * This hook registers session shortcut handlers,
@ -51,6 +57,7 @@ import monaco from "../cell/live_editor/monaco";
*/ */
const Session = { const Session = {
mounted() { mounted() {
this.props = getProps(this);
this.state = { this.state = {
focusedCellId: null, focusedCellId: null,
focusedSectionId: null, focusedSectionId: null,
@ -117,7 +124,7 @@ const Session = {
window.addEventListener( window.addEventListener(
"phx:page-loading-stop", "phx:page-loading-stop",
() => { () => {
focusCellFromUrl(this); initializeFocus(this);
}, },
{ once: true } { once: true }
); );
@ -205,6 +212,16 @@ const Session = {
}, },
}; };
function getProps(hook) {
return {
autofocusCellId: getAttributeOrDefault(
hook.el,
"data-autofocus-cell-id",
null
),
};
}
/** /**
* Data of a specific LV client. * Data of a specific LV client.
* *
@ -481,11 +498,13 @@ function handleCellIndicatorsClick(hook, event) {
} }
/** /**
* Focuses cell based on the given URL. * Focuses cell or any other element based on the current
* URL and hook attributes.
*/ */
function focusCellFromUrl(hook) { function initializeFocus(hook) {
const hash = window.location.hash; const hash = window.location.hash;
if (hash) {
if (hash.startsWith("#cell-")) { if (hash.startsWith("#cell-")) {
const cellId = hash.replace(/^#cell-/, ""); const cellId = hash.replace(/^#cell-/, "");
if (getCellById(cellId)) { if (getCellById(cellId)) {
@ -500,6 +519,10 @@ function focusCellFromUrl(hook) {
element.scrollIntoView(); element.scrollIntoView();
} }
} }
} else if (hook.props.autofocusCellId) {
setFocusedCell(hook, hook.props.autofocusCellId, false);
setInsertMode(hook, true);
}
} }
/** /**

View file

@ -398,11 +398,11 @@ defmodule Livebook.Session do
end end
defp init_data(opts) do defp init_data(opts) do
notebook = opts[:notebook] notebook = Keyword.get_lazy(opts, :notebook, &default_notebook/0)
file = opts[:file] file = opts[:file]
origin = opts[:origin] origin = opts[:origin]
data = if(notebook, do: Data.new(notebook), else: Data.new()) data = Data.new(notebook)
data = %{data | origin: origin} data = %{data | origin: origin}
if file do if file do
@ -418,6 +418,10 @@ defmodule Livebook.Session do
end end
end end
defp default_notebook() do
%{Notebook.new() | sections: [%{Section.new() | cells: [Cell.new(:elixir)]}]}
end
defp schedule_autosave(state) do defp schedule_autosave(state) do
if interval_s = state.data.notebook.autosave_interval_s do if interval_s = state.data.notebook.autosave_interval_s do
ref = Process.send_after(self(), :autosave, interval_s * 1000) ref = Process.send_after(self(), :autosave, interval_s * 1000)

View file

@ -38,7 +38,8 @@ defmodule LivebookWeb.SessionLive do
session_pid: session_pid, session_pid: session_pid,
current_user: current_user, current_user: current_user,
self: self(), self: self(),
data_view: data_to_view(data) data_view: data_to_view(data),
autofocus_cell_id: autofocus_cell_id(data.notebook)
) )
|> assign_private(data: data) |> assign_private(data: data)
|> allow_upload(:cell_image, |> allow_upload(:cell_image,
@ -74,7 +75,8 @@ defmodule LivebookWeb.SessionLive do
<div class="flex flex-grow h-full" <div class="flex flex-grow h-full"
id="session" id="session"
data-element="session" data-element="session"
phx-hook="Session"> phx-hook="Session"
data-autofocus-cell-id={@autofocus_cell_id}>
<SidebarHelpers.sidebar> <SidebarHelpers.sidebar>
<SidebarHelpers.logo_item socket={@socket} /> <SidebarHelpers.logo_item socket={@socket} />
<SidebarHelpers.button_item <SidebarHelpers.button_item
@ -952,7 +954,11 @@ defmodule LivebookWeb.SessionLive do
end end
end end
defp after_operation(socket, _prev_socket, {:delete_section, _client_pid, section_id}) do defp after_operation(
socket,
_prev_socket,
{:delete_section, _client_pid, section_id, _delete_cells}
) do
push_event(socket, "section_deleted", %{section_id: section_id}) push_event(socket, "section_deleted", %{section_id: section_id})
end end
@ -1092,6 +1098,9 @@ defmodule LivebookWeb.SessionLive do
defp process_intellisense_response(response, _request), do: response defp process_intellisense_response(response, _request), do: response
defp autofocus_cell_id(%Notebook{sections: [%{cells: [%{id: id, source: ""}]}]}), do: id
defp autofocus_cell_id(_notebook), do: nil
# Builds view-specific structure of data by cherry-picking # Builds view-specific structure of data by cherry-picking
# only the relevant attributes. # only the relevant attributes.
# We then use `@data_view` in the templates and consequently # We then use `@data_view` in the templates and consequently

View file

@ -307,7 +307,7 @@ defmodule Livebook.SessionTest do
Session.save(session_id) Session.save(session_id)
assert_receive {:operation, {:mark_as_not_dirty, _}} assert_receive {:operation, {:mark_as_not_dirty, _}}
assert FileSystem.File.read(file) == {:ok, "# My notebook\n"} assert {:ok, "# My notebook\n" <> _rest} = FileSystem.File.read(file)
end end
@tag :tmp_dir @tag :tmp_dir
@ -325,7 +325,7 @@ defmodule Livebook.SessionTest do
Session.save(session_id) Session.save(session_id)
assert_receive {:operation, {:mark_as_not_dirty, _}} assert_receive {:operation, {:mark_as_not_dirty, _}}
assert FileSystem.File.read(file) == {:ok, "# My notebook\n"} assert {:ok, "# My notebook\n" <> _rest} = FileSystem.File.read(file)
end end
end end
@ -347,7 +347,7 @@ defmodule Livebook.SessionTest do
Session.close(session_id) Session.close(session_id)
assert_receive :session_closed assert_receive :session_closed
assert FileSystem.File.read(file) == {:ok, "# My notebook\n"} assert {:ok, "# My notebook\n" <> _rest} = FileSystem.File.read(file)
end end
test "clears session temporary directory", %{session_id: session_id} do test "clears session temporary directory", %{session_id: session_id} do

View file

@ -63,6 +63,12 @@ defmodule LivebookWeb.SessionControllerTest do
assert conn.resp_body == """ assert conn.resp_body == """
# Untitled notebook # Untitled notebook
## Section
```elixir
```
""" """
SessionSupervisor.close_session(session_id) SessionSupervisor.close_session(session_id)
@ -124,6 +130,8 @@ defmodule LivebookWeb.SessionControllerTest do
assert conn.resp_body == """ assert conn.resp_body == """
# Title: Untitled notebook # Title: Untitled notebook
# ── Section ──
""" """
SessionSupervisor.close_session(session_id) SessionSupervisor.close_session(session_id)

View file

@ -8,7 +8,7 @@ defmodule LivebookWeb.SessionLiveTest do
alias Livebook.Users.User alias Livebook.Users.User
setup do setup do
{:ok, session_id} = SessionSupervisor.create_session() {:ok, session_id} = SessionSupervisor.create_session(notebook: Livebook.Notebook.new())
%{session_id: session_id} %{session_id: session_id}
end end
@ -54,7 +54,7 @@ defmodule LivebookWeb.SessionLiveTest do
cell_id = insert_text_cell(session_id, section_id, :markdown) cell_id = insert_text_cell(session_id, section_id, :markdown)
assert render(view) =~ cell_id assert render(view) =~ "cell-" <> cell_id
end end
test "un-renders a deleted cell", %{conn: conn, session_id: session_id} do test "un-renders a deleted cell", %{conn: conn, session_id: session_id} do
@ -66,7 +66,7 @@ defmodule LivebookWeb.SessionLiveTest do
Session.delete_cell(session_id, cell_id) Session.delete_cell(session_id, cell_id)
wait_for_session_update(session_id) wait_for_session_update(session_id)
refute render(view) =~ cell_id refute render(view) =~ "cell-" <> cell_id
end end
end end