mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-10 22:23:32 +08:00
Start new notebooks with a focused code cell (#527)
* Start new notebooks with a focused code cell * Update changelog
This commit is contained in:
parent
806101aa25
commit
2b1cb57a15
7 changed files with 71 additions and 26 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue