mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-12-16 21:28:03 +08:00
Add support for smart cell editor (#1050)
* Add support for smart cell editor * Log an error when smart cell fails to start
This commit is contained in:
parent
e6a9cf94ea
commit
6db36ea7e6
30 changed files with 717 additions and 404 deletions
|
|
@ -151,7 +151,7 @@ Also some spacing adjustments.
|
||||||
|
|
||||||
/* When in the first line, we want to display cursor and label in the same line */
|
/* When in the first line, we want to display cursor and label in the same line */
|
||||||
.monaco-cursor-widget-container.inline {
|
.monaco-cursor-widget-container.inline {
|
||||||
display: flex;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monaco-cursor-widget-container.inline .monaco-cursor-widget-label {
|
.monaco-cursor-widget-container.inline .monaco-cursor-widget-label {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import topbar from "topbar";
|
||||||
import { LiveSocket } from "phoenix_live_view";
|
import { LiveSocket } from "phoenix_live_view";
|
||||||
import Headline from "./headline";
|
import Headline from "./headline";
|
||||||
import Cell from "./cell";
|
import Cell from "./cell";
|
||||||
|
import CellEditor from "./cell_editor";
|
||||||
import Session from "./session";
|
import Session from "./session";
|
||||||
import FocusOnUpdate from "./focus_on_update";
|
import FocusOnUpdate from "./focus_on_update";
|
||||||
import ScrollOnUpdate from "./scroll_on_update";
|
import ScrollOnUpdate from "./scroll_on_update";
|
||||||
|
|
@ -34,6 +35,7 @@ import { settingsStore } from "./lib/settings";
|
||||||
const hooks = {
|
const hooks = {
|
||||||
Headline,
|
Headline,
|
||||||
Cell,
|
Cell,
|
||||||
|
CellEditor,
|
||||||
Session,
|
Session,
|
||||||
FocusOnUpdate,
|
FocusOnUpdate,
|
||||||
ScrollOnUpdate,
|
ScrollOnUpdate,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { getAttributeOrThrow } from "../lib/attribute";
|
import { getAttributeOrDefault, getAttributeOrThrow } from "../lib/attribute";
|
||||||
import LiveEditor from "./live_editor";
|
import Markdown from "../lib/markdown";
|
||||||
import Markdown from "./markdown";
|
|
||||||
import { globalPubSub } from "../lib/pub_sub";
|
import { globalPubSub } from "../lib/pub_sub";
|
||||||
import { md5Base64, smoothlyScrollToElement } from "../lib/utils";
|
import { md5Base64, smoothlyScrollToElement } from "../lib/utils";
|
||||||
import scrollIntoView from "scroll-into-view-if-needed";
|
import scrollIntoView from "scroll-into-view-if-needed";
|
||||||
|
|
@ -17,6 +16,7 @@ import { isEvaluable } from "../lib/notebook";
|
||||||
* * `data-cell-id` - id of the cell being edited
|
* * `data-cell-id` - id of the cell being edited
|
||||||
* * `data-type` - type of the cell
|
* * `data-type` - type of the cell
|
||||||
* * `data-session-path` - root path to the current session
|
* * `data-session-path` - root path to the current session
|
||||||
|
* * `data-evaluation-digest` - digest of the last evaluated cell source
|
||||||
*/
|
*/
|
||||||
const Cell = {
|
const Cell = {
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
@ -24,13 +24,13 @@ const Cell = {
|
||||||
this.state = {
|
this.state = {
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
insertMode: false,
|
insertMode: false,
|
||||||
// For text cells (markdown or code)
|
liveEditors: {},
|
||||||
liveEditor: null,
|
|
||||||
markdown: null,
|
|
||||||
evaluationDigest: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateInsertModeAvailability(this);
|
||||||
|
|
||||||
// Setup action handlers
|
// Setup action handlers
|
||||||
|
|
||||||
if (this.props.type === "code") {
|
if (this.props.type === "code") {
|
||||||
const amplifyButton = this.el.querySelector(
|
const amplifyButton = this.el.querySelector(
|
||||||
`[data-element="amplify-outputs-button"]`
|
`[data-element="amplify-outputs-button"]`
|
||||||
|
|
@ -46,119 +46,21 @@ const Cell = {
|
||||||
);
|
);
|
||||||
toggleSourceButton.addEventListener("click", (event) => {
|
toggleSourceButton.addEventListener("click", (event) => {
|
||||||
this.el.toggleAttribute("data-js-source-visible");
|
this.el.toggleAttribute("data-js-source-visible");
|
||||||
|
updateInsertModeAvailability(this);
|
||||||
|
maybeFocusCurrentEditor(this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleEvent(`cell_init:${this.props.cellId}`, (payload) => {
|
// Setup listeners
|
||||||
const { source, revision, evaluation_digest } = payload;
|
|
||||||
|
|
||||||
// Setup markdown rendering
|
this.el.addEventListener("lb:cell:editor_created", (event) => {
|
||||||
if (this.props.type === "markdown") {
|
const { tag, liveEditor } = event.detail;
|
||||||
const markdownContainer = this.el.querySelector(
|
handleCellEditorCreated(this, tag, liveEditor);
|
||||||
`[data-element="markdown-container"]`
|
|
||||||
);
|
|
||||||
this.state.markdown = new Markdown(markdownContainer, source, {
|
|
||||||
baseUrl: this.props.sessionPath,
|
|
||||||
emptyText: "Empty markdown cell",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const editorContainer = this.el.querySelector(
|
|
||||||
`[data-element="editor-container"]`
|
|
||||||
);
|
|
||||||
// Remove the content placeholder.
|
|
||||||
editorContainer.firstElementChild.remove();
|
|
||||||
// Create an empty container for the editor to be mounted in.
|
|
||||||
const editorElement = document.createElement("div");
|
|
||||||
editorContainer.appendChild(editorElement);
|
|
||||||
// Setup the editor instance.
|
|
||||||
const language = {
|
|
||||||
markdown: "markdown",
|
|
||||||
code: "elixir",
|
|
||||||
smart: "elixir",
|
|
||||||
}[this.props.type];
|
|
||||||
const readOnly = this.props.type === "smart";
|
|
||||||
this.state.liveEditor = new LiveEditor(
|
|
||||||
this,
|
|
||||||
editorElement,
|
|
||||||
this.props.cellId,
|
|
||||||
source,
|
|
||||||
revision,
|
|
||||||
language,
|
|
||||||
readOnly
|
|
||||||
);
|
|
||||||
|
|
||||||
// Setup change indicator
|
|
||||||
if (isEvaluable(this.props.type)) {
|
|
||||||
this.state.evaluationDigest = evaluation_digest;
|
|
||||||
|
|
||||||
const updateChangeIndicator = () => {
|
|
||||||
const cellStatus = this.el.querySelector(
|
|
||||||
`[data-element="cell-status"]`
|
|
||||||
);
|
|
||||||
const indicator =
|
|
||||||
cellStatus &&
|
|
||||||
cellStatus.querySelector(`[data-element="change-indicator"]`);
|
|
||||||
|
|
||||||
if (indicator) {
|
|
||||||
const source = this.state.liveEditor.getSource();
|
|
||||||
const digest = md5Base64(source);
|
|
||||||
const changed = this.state.evaluationDigest !== digest;
|
|
||||||
cellStatus.toggleAttribute("data-js-changed", changed);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateChangeIndicator();
|
|
||||||
|
|
||||||
this.handleEvent(
|
|
||||||
`evaluation_started:${this.props.cellId}`,
|
|
||||||
({ evaluation_digest }) => {
|
|
||||||
this.state.evaluationDigest = evaluation_digest;
|
|
||||||
updateChangeIndicator();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.state.liveEditor.onChange((newSource) => {
|
|
||||||
updateChangeIndicator();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.handleEvent(
|
this.el.addEventListener("lb:cell:editor_removed", (event) => {
|
||||||
`evaluation_finished:${this.props.cellId}`,
|
const { tag } = event.detail;
|
||||||
({ code_error }) => {
|
handleCellEditorRemoved(this, tag);
|
||||||
this.state.liveEditor.setCodeErrorMarker(code_error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup markdown updates
|
|
||||||
if (this.props.type === "markdown") {
|
|
||||||
this.state.liveEditor.onChange((newSource) => {
|
|
||||||
this.state.markdown.setContent(newSource);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once the editor is created, reflect the current state.
|
|
||||||
if (this.state.isFocused && this.state.insertMode) {
|
|
||||||
this.state.liveEditor.focus();
|
|
||||||
// If the element is being scrolled to, focus interrupts it,
|
|
||||||
// so ensure the scrolling continues.
|
|
||||||
smoothlyScrollToElement(this.el);
|
|
||||||
|
|
||||||
broadcastSelection(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.liveEditor.onBlur(() => {
|
|
||||||
// Prevent from blurring unless the state changes.
|
|
||||||
// For example when we move cell using buttons
|
|
||||||
// the editor should keep focus.
|
|
||||||
if (this.state.isFocused && this.state.insertMode) {
|
|
||||||
this.state.liveEditor.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.state.liveEditor.onCursorSelectionChange((selection) => {
|
|
||||||
broadcastSelection(this, selection);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this._unsubscribeFromNavigationEvents = globalPubSub.subscribe(
|
this._unsubscribeFromNavigationEvents = globalPubSub.subscribe(
|
||||||
|
|
@ -187,14 +89,15 @@ const Cell = {
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this._unsubscribeFromNavigationEvents();
|
this._unsubscribeFromNavigationEvents();
|
||||||
this._unsubscribeFromCellsEvents();
|
this._unsubscribeFromCellsEvents();
|
||||||
|
|
||||||
if (this.state.liveEditor) {
|
|
||||||
this.state.liveEditor.dispose();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updated() {
|
updated() {
|
||||||
|
const prevProps = this.props;
|
||||||
this.props = getProps(this);
|
this.props = getProps(this);
|
||||||
|
|
||||||
|
if (this.props.evaluationDigest !== prevProps.evaluationDigest) {
|
||||||
|
updateChangeIndicator(this);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -203,6 +106,11 @@ function getProps(hook) {
|
||||||
cellId: getAttributeOrThrow(hook.el, "data-cell-id"),
|
cellId: getAttributeOrThrow(hook.el, "data-cell-id"),
|
||||||
type: getAttributeOrThrow(hook.el, "data-type"),
|
type: getAttributeOrThrow(hook.el, "data-type"),
|
||||||
sessionPath: getAttributeOrThrow(hook.el, "data-session-path"),
|
sessionPath: getAttributeOrThrow(hook.el, "data-session-path"),
|
||||||
|
evaluationDigest: getAttributeOrDefault(
|
||||||
|
hook.el,
|
||||||
|
"data-evaluation-digest",
|
||||||
|
null
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,12 +151,116 @@ function handleElementFocused(hook, focusableId, scroll) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCellEditorCreated(hook, tag, liveEditor) {
|
||||||
|
hook.state.liveEditors[tag] = liveEditor;
|
||||||
|
|
||||||
|
updateInsertModeAvailability(hook);
|
||||||
|
|
||||||
|
if (liveEditor === currentEditor(hook)) {
|
||||||
|
// Once the editor is created, reflect the current insert mode state
|
||||||
|
maybeFocusCurrentEditor(hook, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
liveEditor.onBlur(() => {
|
||||||
|
// Prevent from blurring unless the state changes. For example
|
||||||
|
// when we move cell using buttons the editor should keep focus
|
||||||
|
if (hook.state.isFocused && hook.state.insertMode) {
|
||||||
|
currentEditor(hook).focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
liveEditor.onCursorSelectionChange((selection) => {
|
||||||
|
broadcastSelection(hook, selection);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tag === "primary") {
|
||||||
|
// Setup markdown rendering
|
||||||
|
if (hook.props.type === "markdown") {
|
||||||
|
const markdownContainer = hook.el.querySelector(
|
||||||
|
`[data-element="markdown-container"]`
|
||||||
|
);
|
||||||
|
const markdown = new Markdown(markdownContainer, liveEditor.getSource(), {
|
||||||
|
baseUrl: hook.props.sessionPath,
|
||||||
|
emptyText: "Empty markdown cell",
|
||||||
|
});
|
||||||
|
|
||||||
|
liveEditor.onChange((newSource) => {
|
||||||
|
markdown.setContent(newSource);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup change indicator
|
||||||
|
if (isEvaluable(hook.props.type)) {
|
||||||
|
updateChangeIndicator(hook);
|
||||||
|
|
||||||
|
liveEditor.onChange((newSource) => {
|
||||||
|
updateChangeIndicator(hook);
|
||||||
|
});
|
||||||
|
|
||||||
|
hook.handleEvent(
|
||||||
|
`evaluation_finished:${hook.props.cellId}`,
|
||||||
|
({ code_error }) => {
|
||||||
|
liveEditor.setCodeErrorMarker(code_error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCellEditorRemoved(hook, tag) {
|
||||||
|
delete hook.state.liveEditors[tag];
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentEditor(hook) {
|
||||||
|
return hook.state.liveEditors[currentEditorTag(hook)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentEditorTag(hook) {
|
||||||
|
if (hook.props.type === "smart") {
|
||||||
|
const isSourceTab = hook.el.hasAttribute("data-js-source-visible");
|
||||||
|
return isSourceTab ? "primary" : "secondary";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "primary";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInsertModeAvailability(hook) {
|
||||||
|
hook.el.toggleAttribute("data-js-insert-mode-disabled", !currentEditor(hook));
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeFocusCurrentEditor(hook, scroll = false) {
|
||||||
|
if (hook.state.isFocused && hook.state.insertMode) {
|
||||||
|
currentEditor(hook).focus();
|
||||||
|
|
||||||
|
if (scroll) {
|
||||||
|
// If the element is being scrolled to, focus interrupts it,
|
||||||
|
// so ensure the scrolling continues.
|
||||||
|
smoothlyScrollToElement(hook.el);
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastSelection(hook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChangeIndicator(hook) {
|
||||||
|
const cellStatus = hook.el.querySelector(`[data-element="cell-status"]`);
|
||||||
|
const indicator =
|
||||||
|
cellStatus && cellStatus.querySelector(`[data-element="change-indicator"]`);
|
||||||
|
|
||||||
|
if (indicator && hook.props.evaluationDigest) {
|
||||||
|
const source = hook.state.liveEditors.primary.getSource();
|
||||||
|
const digest = md5Base64(source);
|
||||||
|
const changed = hook.props.evaluationDigest !== digest;
|
||||||
|
cellStatus.toggleAttribute("data-js-changed", changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleInsertModeChanged(hook, insertMode) {
|
function handleInsertModeChanged(hook, insertMode) {
|
||||||
if (hook.state.isFocused && !hook.state.insertMode && insertMode) {
|
if (hook.state.isFocused && !hook.state.insertMode && insertMode) {
|
||||||
hook.state.insertMode = insertMode;
|
hook.state.insertMode = insertMode;
|
||||||
|
|
||||||
if (hook.state.liveEditor) {
|
if (currentEditor(hook)) {
|
||||||
hook.state.liveEditor.focus();
|
currentEditor(hook).focus();
|
||||||
|
|
||||||
// The insert mode may be enabled as a result of clicking the editor,
|
// The insert mode may be enabled as a result of clicking the editor,
|
||||||
// in which case we want to wait until editor handles the click and
|
// in which case we want to wait until editor handles the click and
|
||||||
|
|
@ -268,8 +280,8 @@ function handleInsertModeChanged(hook, insertMode) {
|
||||||
} else if (hook.state.insertMode && !insertMode) {
|
} else if (hook.state.insertMode && !insertMode) {
|
||||||
hook.state.insertMode = insertMode;
|
hook.state.insertMode = insertMode;
|
||||||
|
|
||||||
if (hook.state.liveEditor) {
|
if (currentEditor(hook)) {
|
||||||
hook.state.liveEditor.blur();
|
currentEditor(hook).blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,37 +293,44 @@ function handleCellMoved(hook, cellId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCellUpload(hook, cellId, url) {
|
function handleCellUpload(hook, cellId, url) {
|
||||||
if (!hook.state.liveEditor) {
|
const liveEditor = hook.state.liveEditors.primary;
|
||||||
|
|
||||||
|
if (!liveEditor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hook.props.cellId === cellId) {
|
if (hook.props.cellId === cellId) {
|
||||||
const markdown = ``;
|
const markdown = ``;
|
||||||
hook.state.liveEditor.insert(markdown);
|
liveEditor.insert(markdown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLocationReport(hook, client, report) {
|
function handleLocationReport(hook, client, report) {
|
||||||
if (!hook.state.liveEditor) {
|
Object.entries(hook.state.liveEditors).forEach(([tag, liveEditor]) => {
|
||||||
return;
|
if (
|
||||||
}
|
hook.props.cellId === report.focusableId &&
|
||||||
|
report.selection &&
|
||||||
if (hook.props.cellId === report.focusableId && report.selection) {
|
report.selection.tag === tag
|
||||||
hook.state.liveEditor.updateUserSelection(client, report.selection);
|
) {
|
||||||
|
liveEditor.updateUserSelection(client, report.selection.editorSelection);
|
||||||
} else {
|
} else {
|
||||||
hook.state.liveEditor.removeUserSelection(client);
|
liveEditor.removeUserSelection(client);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastSelection(hook, selection = null) {
|
function broadcastSelection(hook, editorSelection = null) {
|
||||||
selection = selection || hook.state.liveEditor.editor.getSelection();
|
editorSelection =
|
||||||
|
editorSelection || currentEditor(hook).editor.getSelection();
|
||||||
|
|
||||||
|
const tag = currentEditorTag(hook);
|
||||||
|
|
||||||
// Report new selection only if this cell is in insert mode
|
// Report new selection only if this cell is in insert mode
|
||||||
if (hook.state.isFocused && hook.state.insertMode) {
|
if (hook.state.isFocused && hook.state.insertMode) {
|
||||||
globalPubSub.broadcast("session", {
|
globalPubSub.broadcast("session", {
|
||||||
type: "cursor_selection_changed",
|
type: "cursor_selection_changed",
|
||||||
focusableId: hook.props.cellId,
|
focusableId: hook.props.cellId,
|
||||||
selection,
|
selection: { tag, editorSelection },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
assets/js/cell_editor/index.js
Normal file
62
assets/js/cell_editor/index.js
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import LiveEditor from "./live_editor";
|
||||||
|
import { getAttributeOrThrow } from "../lib/attribute";
|
||||||
|
|
||||||
|
const CellEditor = {
|
||||||
|
mounted() {
|
||||||
|
this.props = getProps(this);
|
||||||
|
|
||||||
|
this.handleEvent(
|
||||||
|
`cell_editor_init:${this.props.cellId}:${this.props.tag}`,
|
||||||
|
({ source_view, language, intellisense, read_only }) => {
|
||||||
|
const editorContainer = this.el.querySelector(
|
||||||
|
`[data-element="editor-container"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove the content placeholder
|
||||||
|
editorContainer.firstElementChild.remove();
|
||||||
|
const editorEl = document.createElement("div");
|
||||||
|
editorContainer.appendChild(editorEl);
|
||||||
|
|
||||||
|
this.liveEditor = new LiveEditor(
|
||||||
|
this,
|
||||||
|
editorEl,
|
||||||
|
this.props.cellId,
|
||||||
|
this.props.tag,
|
||||||
|
source_view.source,
|
||||||
|
source_view.revision,
|
||||||
|
language,
|
||||||
|
intellisense,
|
||||||
|
read_only
|
||||||
|
);
|
||||||
|
|
||||||
|
this.el.dispatchEvent(
|
||||||
|
new CustomEvent("lb:cell:editor_created", {
|
||||||
|
detail: { tag: this.props.tag, liveEditor: this.liveEditor },
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
if (this.liveEditor) {
|
||||||
|
this.el.dispatchEvent(
|
||||||
|
new CustomEvent("lb:cell:editor_removed", {
|
||||||
|
detail: { tag: this.props.tag },
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.liveEditor.dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getProps(hook) {
|
||||||
|
return {
|
||||||
|
cellId: getAttributeOrThrow(hook.el, "data-cell-id"),
|
||||||
|
tag: getAttributeOrThrow(hook.el, "data-tag"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CellEditor;
|
||||||
|
|
@ -10,12 +10,23 @@ import { settingsStore } from "../lib/settings";
|
||||||
* Mounts cell source editor with real-time collaboration mechanism.
|
* Mounts cell source editor with real-time collaboration mechanism.
|
||||||
*/
|
*/
|
||||||
class LiveEditor {
|
class LiveEditor {
|
||||||
constructor(hook, container, cellId, source, revision, language, readOnly) {
|
constructor(
|
||||||
|
hook,
|
||||||
|
container,
|
||||||
|
cellId,
|
||||||
|
tag,
|
||||||
|
source,
|
||||||
|
revision,
|
||||||
|
language,
|
||||||
|
intellisense,
|
||||||
|
readOnly
|
||||||
|
) {
|
||||||
this.hook = hook;
|
this.hook = hook;
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.cellId = cellId;
|
this.cellId = cellId;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.language = language;
|
this.language = language;
|
||||||
|
this.intellisense = intellisense;
|
||||||
this.readOnly = readOnly;
|
this.readOnly = readOnly;
|
||||||
this._onChange = null;
|
this._onChange = null;
|
||||||
this._onBlur = null;
|
this._onBlur = null;
|
||||||
|
|
@ -24,11 +35,11 @@ class LiveEditor {
|
||||||
|
|
||||||
this.__mountEditor();
|
this.__mountEditor();
|
||||||
|
|
||||||
if (language === "elixir") {
|
if (this.intellisense) {
|
||||||
this.__setupIntellisense();
|
this.__setupIntellisense();
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverAdapter = new HookServerAdapter(hook, cellId);
|
const serverAdapter = new HookServerAdapter(hook, cellId, tag);
|
||||||
const editorAdapter = new MonacoEditorAdapter(this.editor);
|
const editorAdapter = new MonacoEditorAdapter(this.editor);
|
||||||
this.editorClient = new EditorClient(
|
this.editorClient = new EditorClient(
|
||||||
serverAdapter,
|
serverAdapter,
|
||||||
|
|
@ -147,7 +158,7 @@ class LiveEditor {
|
||||||
* To clear an existing marker `null` error is also supported.
|
* To clear an existing marker `null` error is also supported.
|
||||||
*/
|
*/
|
||||||
setCodeErrorMarker(error) {
|
setCodeErrorMarker(error) {
|
||||||
const owner = "elixir.error.syntax";
|
const owner = "livebook.error.syntax";
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const line = this.editor.getModel().getLineContent(error.line);
|
const line = this.editor.getModel().getLineContent(error.line);
|
||||||
|
|
@ -198,17 +209,15 @@ class LiveEditor {
|
||||||
autoIndent: true,
|
autoIndent: true,
|
||||||
formatOnType: true,
|
formatOnType: true,
|
||||||
formatOnPaste: true,
|
formatOnPaste: true,
|
||||||
quickSuggestions:
|
quickSuggestions: this.intellisense && settings.editor_auto_completion,
|
||||||
this.language === "elixir" && settings.editor_auto_completion,
|
|
||||||
tabCompletion: "on",
|
tabCompletion: "on",
|
||||||
suggestSelection: "first",
|
suggestSelection: "first",
|
||||||
// For Elixir word suggestions are confusing at times.
|
// For Elixir word suggestions are confusing at times.
|
||||||
// For example given `defmodule<CURSOR> Foo do`, if the
|
// For example given `defmodule<CURSOR> Foo do`, if the
|
||||||
// user opens completion list and then jumps to the end
|
// user opens completion list and then jumps to the end
|
||||||
// of the line we would get "defmodule" as a word completion.
|
// of the line we would get "defmodule" as a word completion.
|
||||||
wordBasedSuggestions: this.language !== "elixir",
|
wordBasedSuggestions: !this.intellisense,
|
||||||
parameterHints:
|
parameterHints: this.intellisense && settings.editor_auto_signature,
|
||||||
this.language === "elixir" && settings.editor_auto_signature,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editor.addAction({
|
this.editor.addAction({
|
||||||
|
|
@ -219,6 +228,7 @@ class LiveEditor {
|
||||||
keybindings: [monaco.KeyMod.Alt | monaco.KeyCode.KeyZ],
|
keybindings: [monaco.KeyMod.Alt | monaco.KeyCode.KeyZ],
|
||||||
run: (editor) => editor.updateOptions({ wordWrap: "on" }),
|
run: (editor) => editor.updateOptions({ wordWrap: "on" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editor.addAction({
|
this.editor.addAction({
|
||||||
contextMenuGroupId: "word-wrapping",
|
contextMenuGroupId: "word-wrapping",
|
||||||
id: "disable-word-wrapping",
|
id: "disable-word-wrapping",
|
||||||
|
|
@ -6,19 +6,26 @@ import Delta from "../../lib/delta";
|
||||||
* Uses the given hook instance socket for the communication.
|
* Uses the given hook instance socket for the communication.
|
||||||
*/
|
*/
|
||||||
export default class HookServerAdapter {
|
export default class HookServerAdapter {
|
||||||
constructor(hook, cellId) {
|
constructor(hook, cellId, tag) {
|
||||||
this.hook = hook;
|
this.hook = hook;
|
||||||
this.cellId = cellId;
|
this.cellId = cellId;
|
||||||
|
this.tag = tag;
|
||||||
this._onDelta = null;
|
this._onDelta = null;
|
||||||
this._onAcknowledgement = null;
|
this._onAcknowledgement = null;
|
||||||
|
|
||||||
this.hook.handleEvent(`cell_delta:${this.cellId}`, ({ delta }) => {
|
this.hook.handleEvent(
|
||||||
|
`cell_delta:${this.cellId}:${this.tag}`,
|
||||||
|
({ delta }) => {
|
||||||
this._onDelta && this._onDelta(Delta.fromCompressed(delta));
|
this._onDelta && this._onDelta(Delta.fromCompressed(delta));
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.hook.handleEvent(`cell_acknowledgement:${this.cellId}`, () => {
|
this.hook.handleEvent(
|
||||||
|
`cell_acknowledgement:${this.cellId}:${this.tag}`,
|
||||||
|
() => {
|
||||||
this._onAcknowledgement && this._onAcknowledgement();
|
this._onAcknowledgement && this._onAcknowledgement();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,6 +48,7 @@ export default class HookServerAdapter {
|
||||||
sendDelta(delta, revision) {
|
sendDelta(delta, revision) {
|
||||||
this.hook.pushEvent("apply_cell_delta", {
|
this.hook.pushEvent("apply_cell_delta", {
|
||||||
cell_id: this.cellId,
|
cell_id: this.cellId,
|
||||||
|
tag: this.tag,
|
||||||
delta: delta.toCompressed(),
|
delta: delta.toCompressed(),
|
||||||
revision,
|
revision,
|
||||||
});
|
});
|
||||||
|
|
@ -56,6 +64,7 @@ export default class HookServerAdapter {
|
||||||
reportRevision(revision) {
|
reportRevision(revision) {
|
||||||
this.hook.pushEvent("report_cell_revision", {
|
this.hook.pushEvent("report_cell_revision", {
|
||||||
cell_id: this.cellId,
|
cell_id: this.cellId,
|
||||||
|
tag: this.tag,
|
||||||
revision,
|
revision,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getAttributeOrThrow } from "../lib/attribute";
|
import { getAttributeOrThrow } from "../lib/attribute";
|
||||||
import { highlight } from "../cell/live_editor/monaco";
|
import { highlight } from "../cell_editor/live_editor/monaco";
|
||||||
import { findChildOrThrow } from "../lib/utils";
|
import { findChildOrThrow } from "../lib/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -269,15 +269,22 @@ function bindIframeSize(iframe, iframePlaceholder) {
|
||||||
);
|
);
|
||||||
|
|
||||||
function repositionIframe() {
|
function repositionIframe() {
|
||||||
|
if (iframePlaceholder.offsetParent === null) {
|
||||||
|
// When the placeholder is hidden, we hide the iframe as well
|
||||||
|
iframe.classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
iframe.classList.remove("hidden");
|
||||||
const notebookBox = notebookEl.getBoundingClientRect();
|
const notebookBox = notebookEl.getBoundingClientRect();
|
||||||
const placeholderBox = iframePlaceholder.getBoundingClientRect();
|
const placeholderBox = iframePlaceholder.getBoundingClientRect();
|
||||||
const top = placeholderBox.top - notebookBox.top + notebookEl.scrollTop;
|
const top = placeholderBox.top - notebookBox.top + notebookEl.scrollTop;
|
||||||
iframe.style.top = `${top}px`;
|
iframe.style.top = `${top}px`;
|
||||||
const left = placeholderBox.left - notebookBox.left + notebookEl.scrollLeft;
|
const left =
|
||||||
|
placeholderBox.left - notebookBox.left + notebookEl.scrollLeft;
|
||||||
iframe.style.left = `${left}px`;
|
iframe.style.left = `${left}px`;
|
||||||
iframe.style.height = `${placeholderBox.height}px`;
|
iframe.style.height = `${placeholderBox.height}px`;
|
||||||
iframe.style.width = `${placeholderBox.width}px`;
|
iframe.style.width = `${placeholderBox.width}px`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Most placeholder position changes are accompanied by changes to the
|
// Most placeholder position changes are accompanied by changes to the
|
||||||
// notebook content element height (adding cells, inserting newlines
|
// notebook content element height (adding cells, inserting newlines
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { visit } from "unist-util-visit";
|
||||||
import { toText } from "hast-util-to-text";
|
import { toText } from "hast-util-to-text";
|
||||||
import { removePosition } from "unist-util-remove-position";
|
import { removePosition } from "unist-util-remove-position";
|
||||||
|
|
||||||
import { highlight } from "./live_editor/monaco";
|
import { highlight } from "../cell_editor/live_editor/monaco";
|
||||||
import { renderMermaid } from "./markdown/mermaid";
|
import { renderMermaid } from "./markdown/mermaid";
|
||||||
import { escapeHtml } from "../lib/utils";
|
import { escapeHtml } from "../lib/utils";
|
||||||
|
|
||||||
|
|
@ -6,7 +6,7 @@ export function isEvaluable(cellType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given cell type has editable editor.
|
* Checks if the given cell type has primary editable editor.
|
||||||
*/
|
*/
|
||||||
export function isDirectlyEditable(cellType) {
|
export function isDirectlyEditable(cellType) {
|
||||||
return ["markdown", "code"].includes(cellType);
|
return ["markdown", "code"].includes(cellType);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getAttributeOrThrow } from "../lib/attribute";
|
import { getAttributeOrThrow } from "../lib/attribute";
|
||||||
import Markdown from "../cell/markdown";
|
import Markdown from "../lib/markdown";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook used to render markdown content on the client.
|
* A hook used to render markdown content on the client.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { getAttributeOrDefault } from "../lib/attribute";
|
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_editor/live_editor/monaco";
|
||||||
import { leaveChannel } from "../js_view";
|
import { leaveChannel } from "../js_view";
|
||||||
import { isDirectlyEditable, isEvaluable } from "../lib/notebook";
|
import { isDirectlyEditable, isEvaluable } from "../lib/notebook";
|
||||||
|
|
||||||
|
|
@ -477,12 +477,12 @@ function handleDocumentMouseDown(hook, event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function editableElementClicked(event, element) {
|
function editableElementClicked(event, focusableEl) {
|
||||||
if (element) {
|
if (focusableEl) {
|
||||||
const editableElement = element.querySelector(
|
const editableElement = event.target.closest(
|
||||||
`[data-element="editor-container"], [data-element="heading"]`
|
`[data-element="editor-container"], [data-element="heading"]`
|
||||||
);
|
);
|
||||||
return editableElement && editableElement.contains(event.target);
|
return editableElement && focusableEl.contains(editableElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -748,9 +748,11 @@ function showShortcuts(hook) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInsertModeAvailable(hook) {
|
function isInsertModeAvailable(hook) {
|
||||||
|
const el = getFocusableEl(hook.state.focusedId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
hook.state.focusedCellType === null ||
|
!isCell(hook.state.focusedId) ||
|
||||||
isDirectlyEditable(hook.state.focusedCellType)
|
!el.hasAttribute("data-js-insert-mode-disabled")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1011,11 +1013,14 @@ function sendLocationReport(hook, report) {
|
||||||
function encodeSelection(selection) {
|
function encodeSelection(selection) {
|
||||||
if (selection === null) return null;
|
if (selection === null) return null;
|
||||||
|
|
||||||
|
const { tag, editorSelection } = selection;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
selection.selectionStartLineNumber,
|
tag,
|
||||||
selection.selectionStartColumn,
|
editorSelection.selectionStartLineNumber,
|
||||||
selection.positionLineNumber,
|
editorSelection.selectionStartColumn,
|
||||||
selection.positionColumn,
|
editorSelection.positionLineNumber,
|
||||||
|
editorSelection.positionColumn,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1023,18 +1028,21 @@ function decodeSelection(encoded) {
|
||||||
if (encoded === null) return null;
|
if (encoded === null) return null;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
tag,
|
||||||
selectionStartLineNumber,
|
selectionStartLineNumber,
|
||||||
selectionStartColumn,
|
selectionStartColumn,
|
||||||
positionLineNumber,
|
positionLineNumber,
|
||||||
positionColumn,
|
positionColumn,
|
||||||
] = encoded;
|
] = encoded;
|
||||||
|
|
||||||
return new monaco.Selection(
|
const editorSelection = new monaco.Selection(
|
||||||
selectionStartLineNumber,
|
selectionStartLineNumber,
|
||||||
selectionStartColumn,
|
selectionStartColumn,
|
||||||
positionLineNumber,
|
positionLineNumber,
|
||||||
positionColumn
|
positionColumn
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return { tag, editorSelection };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ defmodule Livebook.Notebook.Cell.Smart do
|
||||||
|
|
||||||
# A cell with Elixir code that is edited through a dedicated UI.
|
# A cell with Elixir code that is edited through a dedicated UI.
|
||||||
|
|
||||||
defstruct [:id, :source, :outputs, :kind, :attrs, :js_view]
|
defstruct [:id, :source, :outputs, :kind, :attrs, :js_view, :editor]
|
||||||
|
|
||||||
alias Livebook.Utils
|
alias Livebook.Utils
|
||||||
alias Livebook.Notebook.Cell
|
alias Livebook.Notebook.Cell
|
||||||
|
|
@ -14,11 +14,14 @@ defmodule Livebook.Notebook.Cell.Smart do
|
||||||
outputs: list(Cell.indexed_output()),
|
outputs: list(Cell.indexed_output()),
|
||||||
kind: String.t(),
|
kind: String.t(),
|
||||||
attrs: attrs(),
|
attrs: attrs(),
|
||||||
js_view: Livebook.Runtime.js_view() | nil
|
js_view: Livebook.Runtime.js_view() | nil,
|
||||||
|
editor: editor() | nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@type attrs :: map()
|
@type attrs :: map()
|
||||||
|
|
||||||
|
@type editor :: %{language: String.t(), placement: :bottom | :top, source: String.t()}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an empty cell.
|
Returns an empty cell.
|
||||||
"""
|
"""
|
||||||
|
|
@ -30,7 +33,8 @@ defmodule Livebook.Notebook.Cell.Smart do
|
||||||
outputs: [],
|
outputs: [],
|
||||||
kind: nil,
|
kind: nil,
|
||||||
attrs: %{},
|
attrs: %{},
|
||||||
js_view: nil
|
js_view: nil,
|
||||||
|
editor: nil
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -377,15 +377,17 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
||||||
) do
|
) do
|
||||||
{:ok, pid, info} ->
|
{:ok, pid, info} ->
|
||||||
%{
|
%{
|
||||||
js_view: js_view,
|
|
||||||
source: source,
|
source: source,
|
||||||
|
js_view: js_view,
|
||||||
|
editor: editor,
|
||||||
scan_binding: scan_binding,
|
scan_binding: scan_binding,
|
||||||
scan_eval_result: scan_eval_result
|
scan_eval_result: scan_eval_result
|
||||||
} = info
|
} = info
|
||||||
|
|
||||||
send(
|
send(
|
||||||
state.owner,
|
state.owner,
|
||||||
{:runtime_smart_cell_started, ref, %{js_view: js_view, source: source}}
|
{:runtime_smart_cell_started, ref,
|
||||||
|
%{source: source, js_view: js_view, editor: editor}}
|
||||||
)
|
)
|
||||||
|
|
||||||
info = %{
|
info = %{
|
||||||
|
|
@ -400,7 +402,8 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
||||||
info = scan_binding_async(ref, info, state)
|
info = scan_binding_async(ref, info, state)
|
||||||
put_in(state.smart_cells[ref], info)
|
put_in(state.smart_cells[ref], info)
|
||||||
|
|
||||||
_ ->
|
{:error, error} ->
|
||||||
|
Logger.error("failed to start smart cell, reason: #{inspect(error)}")
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -380,9 +380,15 @@ defmodule Livebook.Session do
|
||||||
@doc """
|
@doc """
|
||||||
Sends a cell delta to apply to the server.
|
Sends a cell delta to apply to the server.
|
||||||
"""
|
"""
|
||||||
@spec apply_cell_delta(pid(), Cell.id(), Delta.t(), Data.cell_revision()) :: :ok
|
@spec apply_cell_delta(
|
||||||
def apply_cell_delta(pid, cell_id, delta, revision) do
|
pid(),
|
||||||
GenServer.cast(pid, {:apply_cell_delta, self(), cell_id, delta, revision})
|
Cell.id(),
|
||||||
|
Data.cell_source_tag(),
|
||||||
|
Delta.t(),
|
||||||
|
Data.cell_revision()
|
||||||
|
) :: :ok
|
||||||
|
def apply_cell_delta(pid, cell_id, tag, delta, revision) do
|
||||||
|
GenServer.cast(pid, {:apply_cell_delta, self(), cell_id, tag, delta, revision})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -390,9 +396,14 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
This helps to remove old deltas that are no longer necessary.
|
This helps to remove old deltas that are no longer necessary.
|
||||||
"""
|
"""
|
||||||
@spec report_cell_revision(pid(), Cell.id(), Data.cell_revision()) :: :ok
|
@spec report_cell_revision(
|
||||||
def report_cell_revision(pid, cell_id, revision) do
|
pid(),
|
||||||
GenServer.cast(pid, {:report_cell_revision, self(), cell_id, revision})
|
Cell.id(),
|
||||||
|
Data.cell_source_tag(),
|
||||||
|
Data.cell_revision()
|
||||||
|
) :: :ok
|
||||||
|
def report_cell_revision(pid, cell_id, tag, revision) do
|
||||||
|
GenServer.cast(pid, {:report_cell_revision, self(), cell_id, tag, revision})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -770,13 +781,13 @@ defmodule Livebook.Session do
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_cast({:apply_cell_delta, client_pid, cell_id, delta, revision}, state) do
|
def handle_cast({:apply_cell_delta, client_pid, cell_id, tag, delta, revision}, state) do
|
||||||
operation = {:apply_cell_delta, client_pid, cell_id, delta, revision}
|
operation = {:apply_cell_delta, client_pid, cell_id, tag, delta, revision}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_cast({:report_cell_revision, client_pid, cell_id, revision}, state) do
|
def handle_cast({:report_cell_revision, client_pid, cell_id, tag, revision}, state) do
|
||||||
operation = {:report_cell_revision, client_pid, cell_id, revision}
|
operation = {:report_cell_revision, client_pid, cell_id, tag, revision}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -914,7 +925,7 @@ defmodule Livebook.Session do
|
||||||
case Notebook.fetch_cell_and_section(state.data.notebook, id) do
|
case Notebook.fetch_cell_and_section(state.data.notebook, id) do
|
||||||
{:ok, cell, _section} ->
|
{:ok, cell, _section} ->
|
||||||
delta = Livebook.JSInterop.diff(cell.source, info.source)
|
delta = Livebook.JSInterop.diff(cell.source, info.source)
|
||||||
operation = {:smart_cell_started, self(), id, delta, info.js_view}
|
operation = {:smart_cell_started, self(), id, delta, info.js_view, info.editor}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
|
|
@ -922,11 +933,11 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:runtime_smart_cell_update, id, cell_state, source}, state) do
|
def handle_info({:runtime_smart_cell_update, id, attrs, source}, state) do
|
||||||
case Notebook.fetch_cell_and_section(state.data.notebook, id) do
|
case Notebook.fetch_cell_and_section(state.data.notebook, id) do
|
||||||
{:ok, cell, _section} ->
|
{:ok, cell, _section} ->
|
||||||
delta = Livebook.JSInterop.diff(cell.source, source)
|
delta = Livebook.JSInterop.diff(cell.source, source)
|
||||||
operation = {:update_smart_cell, self(), id, cell_state, delta}
|
operation = {:update_smart_cell, self(), id, attrs, delta}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
|
|
@ -1181,6 +1192,20 @@ defmodule Livebook.Session do
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp after_operation(
|
||||||
|
state,
|
||||||
|
_prev_state,
|
||||||
|
{:apply_cell_delta, _client_pid, cell_id, tag, _delta, _revision}
|
||||||
|
) do
|
||||||
|
with :secondary <- tag,
|
||||||
|
{:ok, %Cell.Smart{} = cell, _section} <-
|
||||||
|
Notebook.fetch_cell_and_section(state.data.notebook, cell_id) do
|
||||||
|
send(cell.js_view.pid, {:editor_source, cell.editor.source})
|
||||||
|
end
|
||||||
|
|
||||||
|
state
|
||||||
|
end
|
||||||
|
|
||||||
defp after_operation(state, _prev_state, _operation), do: state
|
defp after_operation(state, _prev_state, _operation), do: state
|
||||||
|
|
||||||
defp handle_actions(state, actions) do
|
defp handle_actions(state, actions) do
|
||||||
|
|
|
||||||
|
|
@ -61,20 +61,22 @@ defmodule Livebook.Session.Data do
|
||||||
@type cell_info :: markdown_cell_info() | code_cell_info() | smart_cell_info()
|
@type cell_info :: markdown_cell_info() | code_cell_info() | smart_cell_info()
|
||||||
|
|
||||||
@type markdown_cell_info :: %{
|
@type markdown_cell_info :: %{
|
||||||
source: cell_source_info()
|
sources: %{primary: cell_source_info()}
|
||||||
}
|
}
|
||||||
|
|
||||||
@type code_cell_info :: %{
|
@type code_cell_info :: %{
|
||||||
source: cell_source_info(),
|
sources: %{primary: cell_source_info()},
|
||||||
eval: cell_eval_info()
|
eval: cell_eval_info()
|
||||||
}
|
}
|
||||||
|
|
||||||
@type smart_cell_info :: %{
|
@type smart_cell_info :: %{
|
||||||
source: cell_source_info(),
|
sources: %{primary: cell_source_info(), secondary: cell_source_info()},
|
||||||
eval: cell_eval_info(),
|
eval: cell_eval_info(),
|
||||||
status: smart_cell_status()
|
status: smart_cell_status()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@type cell_source_tag :: atom()
|
||||||
|
|
||||||
@type cell_source_info :: %{
|
@type cell_source_info :: %{
|
||||||
revision: cell_revision(),
|
revision: cell_revision(),
|
||||||
deltas: list(Delta.t()),
|
deltas: list(Delta.t()),
|
||||||
|
|
@ -163,7 +165,8 @@ defmodule Livebook.Session.Data do
|
||||||
| {:reflect_main_evaluation_failure, pid()}
|
| {:reflect_main_evaluation_failure, pid()}
|
||||||
| {:reflect_evaluation_failure, pid(), Section.id()}
|
| {:reflect_evaluation_failure, pid(), Section.id()}
|
||||||
| {:cancel_cell_evaluation, pid(), Cell.id()}
|
| {:cancel_cell_evaluation, pid(), Cell.id()}
|
||||||
| {:smart_cell_started, pid(), Cell.id(), Delta.t(), Runtime.js_view()}
|
| {:smart_cell_started, pid(), Cell.id(), Delta.t(), Runtime.js_view(),
|
||||||
|
Cell.Smart.editor() | nil}
|
||||||
| {:update_smart_cell, pid(), Cell.id(), Cell.Smart.attrs(), Delta.t()}
|
| {:update_smart_cell, pid(), Cell.id(), Cell.Smart.attrs(), Delta.t()}
|
||||||
| {:erase_outputs, pid()}
|
| {:erase_outputs, pid()}
|
||||||
| {:set_notebook_name, pid(), String.t()}
|
| {:set_notebook_name, pid(), String.t()}
|
||||||
|
|
@ -171,8 +174,8 @@ defmodule Livebook.Session.Data do
|
||||||
| {:client_join, pid(), User.t()}
|
| {:client_join, pid(), User.t()}
|
||||||
| {:client_leave, pid()}
|
| {:client_leave, pid()}
|
||||||
| {:update_user, pid(), User.t()}
|
| {:update_user, pid(), User.t()}
|
||||||
| {:apply_cell_delta, pid(), Cell.id(), Delta.t(), cell_revision()}
|
| {:apply_cell_delta, pid(), Cell.id(), cell_source_tag(), Delta.t(), cell_revision()}
|
||||||
| {:report_cell_revision, pid(), Cell.id(), cell_revision()}
|
| {:report_cell_revision, pid(), Cell.id(), cell_source_tag(), cell_revision()}
|
||||||
| {:set_cell_attributes, pid(), Cell.id(), map()}
|
| {:set_cell_attributes, pid(), Cell.id(), map()}
|
||||||
| {:set_input_value, pid(), input_id(), value :: term()}
|
| {:set_input_value, pid(), input_id(), value :: term()}
|
||||||
| {:set_runtime, pid(), Runtime.t() | nil}
|
| {:set_runtime, pid(), Runtime.t() | nil}
|
||||||
|
|
@ -188,7 +191,7 @@ defmodule Livebook.Session.Data do
|
||||||
| {:forget_evaluation, Cell.t(), Section.t()}
|
| {:forget_evaluation, Cell.t(), Section.t()}
|
||||||
| {:start_smart_cell, Cell.t(), Section.t()}
|
| {:start_smart_cell, Cell.t(), Section.t()}
|
||||||
| {:set_smart_cell_base, Cell.t(), Section.t(), parent :: {Cell.t(), Section.t()} | nil}
|
| {:set_smart_cell_base, Cell.t(), Section.t(), parent :: {Cell.t(), Section.t()} | nil}
|
||||||
| {:broadcast_delta, pid(), Cell.t(), Delta.t()}
|
| {:broadcast_delta, pid(), Cell.t(), cell_source_tag(), Delta.t()}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a fresh notebook session state.
|
Returns a fresh notebook session state.
|
||||||
|
|
@ -541,13 +544,13 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_operation(data, {:smart_cell_started, client_pid, id, delta, js_view}) do
|
def apply_operation(data, {:smart_cell_started, client_pid, id, delta, js_view, editor}) do
|
||||||
with {:ok, %Cell.Smart{} = cell, _section} <-
|
with {:ok, %Cell.Smart{} = cell, _section} <-
|
||||||
Notebook.fetch_cell_and_section(data.notebook, id),
|
Notebook.fetch_cell_and_section(data.notebook, id),
|
||||||
:starting <- data.cell_infos[cell.id].status do
|
:starting <- data.cell_infos[cell.id].status do
|
||||||
data
|
data
|
||||||
|> with_actions()
|
|> with_actions()
|
||||||
|> smart_cell_started(cell, client_pid, delta, js_view)
|
|> smart_cell_started(cell, client_pid, delta, js_view, editor)
|
||||||
|> set_dirty()
|
|> set_dirty()
|
||||||
|> wrap_ok()
|
|> wrap_ok()
|
||||||
else
|
else
|
||||||
|
|
@ -628,14 +631,14 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_operation(data, {:apply_cell_delta, client_pid, cell_id, delta, revision}) do
|
def apply_operation(data, {:apply_cell_delta, client_pid, cell_id, tag, delta, revision}) do
|
||||||
with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
|
with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
|
||||||
info <- data.cell_infos[cell.id],
|
source_info <- data.cell_infos[cell_id].sources[tag],
|
||||||
true <- 0 < revision and revision <= info.source.revision + 1,
|
true <- 0 < revision and revision <= source_info.revision + 1,
|
||||||
true <- Map.has_key?(data.clients_map, client_pid) do
|
true <- Map.has_key?(data.clients_map, client_pid) do
|
||||||
data
|
data
|
||||||
|> with_actions()
|
|> with_actions()
|
||||||
|> apply_delta(client_pid, cell, delta, revision)
|
|> apply_delta(client_pid, cell, tag, delta, revision)
|
||||||
|> set_dirty()
|
|> set_dirty()
|
||||||
|> wrap_ok()
|
|> wrap_ok()
|
||||||
else
|
else
|
||||||
|
|
@ -643,14 +646,14 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_operation(data, {:report_cell_revision, client_pid, cell_id, revision}) do
|
def apply_operation(data, {:report_cell_revision, client_pid, cell_id, tag, revision}) do
|
||||||
with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
|
with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
|
||||||
info <- data.cell_infos[cell.id],
|
source_info <- data.cell_infos[cell_id].sources[tag],
|
||||||
true <- 0 < revision and revision <= info.source.revision,
|
true <- 0 < revision and revision <= source_info.revision,
|
||||||
true <- Map.has_key?(data.clients_map, client_pid) do
|
true <- Map.has_key?(data.clients_map, client_pid) do
|
||||||
data
|
data
|
||||||
|> with_actions()
|
|> with_actions()
|
||||||
|> report_revision(client_pid, cell, revision)
|
|> report_revision(client_pid, cell, tag, revision)
|
||||||
|> wrap_ok()
|
|> wrap_ok()
|
||||||
else
|
else
|
||||||
_ -> :error
|
_ -> :error
|
||||||
|
|
@ -1221,13 +1224,17 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp smart_cell_started({data, _} = data_actions, cell, client_pid, delta, js_view) do
|
defp smart_cell_started({data, _} = data_actions, cell, client_pid, delta, js_view, editor) do
|
||||||
updated_cell = %{cell | js_view: js_view} |> apply_delta_to_cell(delta)
|
updated_cell = %{cell | js_view: js_view, editor: editor} |> apply_delta_to_cell(delta)
|
||||||
|
|
||||||
data_actions
|
data_actions
|
||||||
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, fn _ -> updated_cell end))
|
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, fn _ -> updated_cell end))
|
||||||
|> update_cell_info!(cell.id, &%{&1 | status: :started})
|
|> update_cell_info!(cell.id, &%{&1 | status: :started})
|
||||||
|> add_action({:broadcast_delta, client_pid, updated_cell, delta})
|
|> update_cell_info!(cell.id, fn info ->
|
||||||
|
info = %{info | status: :started}
|
||||||
|
put_in(info.sources.secondary, new_source_info(data.clients_map))
|
||||||
|
end)
|
||||||
|
|> add_action({:broadcast_delta, client_pid, updated_cell, :primary, delta})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp update_smart_cell({data, _} = data_actions, cell, client_pid, attrs, delta) do
|
defp update_smart_cell({data, _} = data_actions, cell, client_pid, attrs, delta) do
|
||||||
|
|
@ -1241,7 +1248,7 @@ defmodule Livebook.Session.Data do
|
||||||
|
|
||||||
data_actions
|
data_actions
|
||||||
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, fn _ -> updated_cell end))
|
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, fn _ -> updated_cell end))
|
||||||
|> add_action({:broadcast_delta, client_pid, updated_cell, delta})
|
|> add_action({:broadcast_delta, client_pid, updated_cell, :primary, delta})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp erase_outputs({data, _} = data_actions) do
|
defp erase_outputs({data, _} = data_actions) do
|
||||||
|
|
@ -1280,8 +1287,13 @@ defmodule Livebook.Session.Data do
|
||||||
users_map: Map.put(data.users_map, user.id, user)
|
users_map: Map.put(data.users_map, user.id, user)
|
||||||
)
|
)
|
||||||
|> update_every_cell_info(fn
|
|> update_every_cell_info(fn
|
||||||
%{source: _} = info ->
|
%{sources: _} = info ->
|
||||||
put_in(info.source.revision_by_client_pid[client_pid], info.source.revision)
|
update_in(
|
||||||
|
info.sources,
|
||||||
|
&Map.map(&1, fn {_, source_info} ->
|
||||||
|
put_in(source_info.revision_by_client_pid[client_pid], source_info.revision)
|
||||||
|
end)
|
||||||
|
)
|
||||||
|
|
||||||
info ->
|
info ->
|
||||||
info
|
info
|
||||||
|
|
@ -1301,9 +1313,14 @@ defmodule Livebook.Session.Data do
|
||||||
data_actions
|
data_actions
|
||||||
|> set!(clients_map: clients_map, users_map: users_map)
|
|> set!(clients_map: clients_map, users_map: users_map)
|
||||||
|> update_every_cell_info(fn
|
|> update_every_cell_info(fn
|
||||||
%{source: _} = info ->
|
%{sources: _} = info ->
|
||||||
{_, info} = pop_in(info.source.revision_by_client_pid[client_pid])
|
update_in(
|
||||||
update_in(info.source, &purge_deltas/1)
|
info.sources,
|
||||||
|
&Map.map(&1, fn {_, source_info} ->
|
||||||
|
{_, source_info} = pop_in(source_info.revision_by_client_pid[client_pid])
|
||||||
|
purge_deltas(source_info)
|
||||||
|
end)
|
||||||
|
)
|
||||||
|
|
||||||
info ->
|
info ->
|
||||||
info
|
info
|
||||||
|
|
@ -1314,36 +1331,43 @@ defmodule Livebook.Session.Data do
|
||||||
set!(data_actions, users_map: Map.put(data.users_map, user.id, user))
|
set!(data_actions, users_map: Map.put(data.users_map, user.id, user))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_delta({data, _} = data_actions, client_pid, cell, delta, revision) do
|
defp apply_delta({data, _} = data_actions, client_pid, cell, tag, delta, revision) do
|
||||||
info = data.cell_infos[cell.id]
|
source_info = data.cell_infos[cell.id].sources[tag]
|
||||||
|
|
||||||
deltas_ahead = Enum.take(info.source.deltas, -(info.source.revision - revision + 1))
|
deltas_ahead = Enum.take(source_info.deltas, -(source_info.revision - revision + 1))
|
||||||
|
|
||||||
transformed_new_delta =
|
transformed_new_delta =
|
||||||
Enum.reduce(deltas_ahead, delta, fn delta_ahead, transformed_new_delta ->
|
Enum.reduce(deltas_ahead, delta, fn delta_ahead, transformed_new_delta ->
|
||||||
Delta.transform(delta_ahead, transformed_new_delta, :left)
|
Delta.transform(delta_ahead, transformed_new_delta, :left)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
updated_cell = apply_delta_to_cell(cell, transformed_new_delta)
|
source_info =
|
||||||
|
|
||||||
data_actions
|
|
||||||
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, fn _ -> updated_cell end))
|
|
||||||
|> update_cell_source_info!(cell.id, fn source_info ->
|
|
||||||
source_info = %{
|
|
||||||
source_info
|
source_info
|
||||||
| deltas: source_info.deltas ++ [transformed_new_delta],
|
|> Map.update!(:deltas, &(&1 ++ [transformed_new_delta]))
|
||||||
revision: source_info.revision + 1
|
|> Map.update!(:revision, &(&1 + 1))
|
||||||
}
|
|
||||||
|
|
||||||
# Before receiving acknowledgement, the client receives all
|
# Before receiving acknowledgement, the client receives all
|
||||||
# the other deltas, so we can assume they are in sync with
|
# the other deltas, so we can assume they are in sync with
|
||||||
# the server and have the same revision.
|
# the server and have the same revision.
|
||||||
|
source_info =
|
||||||
put_in(source_info.revision_by_client_pid[client_pid], source_info.revision)
|
put_in(source_info.revision_by_client_pid[client_pid], source_info.revision)
|
||||||
|> purge_deltas()
|
|> purge_deltas()
|
||||||
|
|
||||||
|
updated_cell =
|
||||||
|
update_in(cell, source_access(cell, tag), fn
|
||||||
|
:__pruned__ -> :__pruned__
|
||||||
|
source -> JSInterop.apply_delta_to_string(transformed_new_delta, source)
|
||||||
end)
|
end)
|
||||||
|> add_action({:broadcast_delta, client_pid, updated_cell, transformed_new_delta})
|
|
||||||
|
data_actions
|
||||||
|
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, fn _ -> updated_cell end))
|
||||||
|
|> update_cell_info!(cell.id, &put_in(&1.sources[tag], source_info))
|
||||||
|
|> add_action({:broadcast_delta, client_pid, updated_cell, tag, transformed_new_delta})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp source_access(%Cell.Smart{}, :secondary), do: [Access.key(:editor), :source]
|
||||||
|
defp source_access(_cell, :primary), do: [Access.key(:source)]
|
||||||
|
|
||||||
# Note: the clients drop cell's source once it's no longer needed
|
# Note: the clients drop cell's source once it's no longer needed
|
||||||
defp apply_delta_to_cell(%{source: :__pruned__} = cell, _delta), do: cell
|
defp apply_delta_to_cell(%{source: :__pruned__} = cell, _delta), do: cell
|
||||||
|
|
||||||
|
|
@ -1351,12 +1375,14 @@ defmodule Livebook.Session.Data do
|
||||||
update_in(cell.source, &JSInterop.apply_delta_to_string(delta, &1))
|
update_in(cell.source, &JSInterop.apply_delta_to_string(delta, &1))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp report_revision(data_actions, client_pid, cell, revision) do
|
defp report_revision(data_actions, client_pid, cell, tag, revision) do
|
||||||
data_actions
|
data_actions
|
||||||
|> update_cell_source_info!(cell.id, fn source_info ->
|
|> update_cell_info!(cell.id, fn info ->
|
||||||
|
update_in(info.sources[tag], fn source_info ->
|
||||||
put_in(source_info.revision_by_client_pid[client_pid], revision)
|
put_in(source_info.revision_by_client_pid[client_pid], revision)
|
||||||
|> purge_deltas()
|
|> purge_deltas()
|
||||||
end)
|
end)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp set_cell_attributes({data, _} = data_actions, cell, attrs) do
|
defp set_cell_attributes({data, _} = data_actions, cell, attrs) do
|
||||||
|
|
@ -1417,7 +1443,7 @@ defmodule Livebook.Session.Data do
|
||||||
Notebook.update_reduce_cells(data.notebook, data_actions, fn
|
Notebook.update_reduce_cells(data.notebook, data_actions, fn
|
||||||
%Cell.Smart{} = cell, data_actions ->
|
%Cell.Smart{} = cell, data_actions ->
|
||||||
{
|
{
|
||||||
%{cell | js_view: nil},
|
%{cell | js_view: nil, editor: nil},
|
||||||
update_cell_info!(data_actions, cell.id, &%{&1 | status: :dead})
|
update_cell_info!(data_actions, cell.id, &%{&1 | status: :dead})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1554,20 +1580,20 @@ defmodule Livebook.Session.Data do
|
||||||
|
|
||||||
defp new_cell_info(%Cell.Markdown{}, clients_map) do
|
defp new_cell_info(%Cell.Markdown{}, clients_map) do
|
||||||
%{
|
%{
|
||||||
source: new_source_info(clients_map)
|
sources: %{primary: new_source_info(clients_map)}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp new_cell_info(%Cell.Code{}, clients_map) do
|
defp new_cell_info(%Cell.Code{}, clients_map) do
|
||||||
%{
|
%{
|
||||||
source: new_source_info(clients_map),
|
sources: %{primary: new_source_info(clients_map)},
|
||||||
eval: new_eval_info()
|
eval: new_eval_info()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp new_cell_info(%Cell.Smart{}, clients_map) do
|
defp new_cell_info(%Cell.Smart{}, clients_map) do
|
||||||
%{
|
%{
|
||||||
source: new_source_info(clients_map),
|
sources: %{primary: new_source_info(clients_map), secondary: new_source_info(clients_map)},
|
||||||
eval: new_eval_info(),
|
eval: new_eval_info(),
|
||||||
status: :dead
|
status: :dead
|
||||||
}
|
}
|
||||||
|
|
@ -1616,10 +1642,6 @@ defmodule Livebook.Session.Data do
|
||||||
update_cell_info!(data_actions, cell_id, &update_in(&1.eval, fun))
|
update_cell_info!(data_actions, cell_id, &update_in(&1.eval, fun))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp update_cell_source_info!(data_actions, cell_id, fun) do
|
|
||||||
update_cell_info!(data_actions, cell_id, &update_in(&1.source, fun))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp update_every_cell_info({data, _} = data_actions, fun) do
|
defp update_every_cell_info({data, _} = data_actions, fun) do
|
||||||
cell_infos =
|
cell_infos =
|
||||||
Map.new(data.cell_infos, fn {cell_id, info} ->
|
Map.new(data.cell_infos, fn {cell_id, info} ->
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,36 @@ defmodule LivebookWeb.Helpers do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a text content skeleton.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
* `:empty` - if the source is empty. Defauls to `false`
|
||||||
|
|
||||||
|
* `:bg_class` - the skeleton background color. Defaults to `"bg-gray-200"`
|
||||||
|
"""
|
||||||
|
def content_skeleton(assigns) do
|
||||||
|
assigns =
|
||||||
|
assigns
|
||||||
|
|> assign_new(:empty, fn -> false end)
|
||||||
|
|> assign_new(:bg_class, fn -> "bg-gray-200" end)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<%= if @empty do %>
|
||||||
|
<div class="h-4"></div>
|
||||||
|
<% else %>
|
||||||
|
<div class="max-w-2xl w-full animate-pulse">
|
||||||
|
<div class="flex-1 space-y-4">
|
||||||
|
<div class={"#{@bg_class} h-4 rounded-lg w-3/4"}></div>
|
||||||
|
<div class={"#{@bg_class} h-4 rounded-lg"}></div>
|
||||||
|
<div class={"#{@bg_class} h-4 rounded-lg w-5/6"}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Determines user platform based on the given *User-Agent* header.
|
Determines user platform based on the given *User-Agent* header.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -648,21 +648,23 @@ defmodule LivebookWeb.SessionLive do
|
||||||
|
|
||||||
def handle_event(
|
def handle_event(
|
||||||
"apply_cell_delta",
|
"apply_cell_delta",
|
||||||
%{"cell_id" => cell_id, "delta" => delta, "revision" => revision},
|
%{"cell_id" => cell_id, "tag" => tag, "delta" => delta, "revision" => revision},
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
|
tag = String.to_atom(tag)
|
||||||
delta = Delta.from_compressed(delta)
|
delta = Delta.from_compressed(delta)
|
||||||
Session.apply_cell_delta(socket.assigns.session.pid, cell_id, delta, revision)
|
Session.apply_cell_delta(socket.assigns.session.pid, cell_id, tag, delta, revision)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event(
|
def handle_event(
|
||||||
"report_cell_revision",
|
"report_cell_revision",
|
||||||
%{"cell_id" => cell_id, "revision" => revision},
|
%{"cell_id" => cell_id, "tag" => tag, "revision" => revision},
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
Session.report_cell_revision(socket.assigns.session.pid, cell_id, revision)
|
tag = String.to_atom(tag)
|
||||||
|
Session.report_cell_revision(socket.assigns.session.pid, cell_id, tag, revision)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
@ -1194,16 +1196,6 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp after_operation(
|
|
||||||
socket,
|
|
||||||
_prev_socket,
|
|
||||||
{:evaluation_started, _client_pid, cell_id, evaluation_digest}
|
|
||||||
) do
|
|
||||||
push_event(socket, "evaluation_started:#{cell_id}", %{
|
|
||||||
evaluation_digest: encode_digest(evaluation_digest)
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp after_operation(
|
defp after_operation(
|
||||||
socket,
|
socket,
|
||||||
_prev_socket,
|
_prev_socket,
|
||||||
|
|
@ -1222,17 +1214,25 @@ defmodule LivebookWeb.SessionLive do
|
||||||
|> push_event("evaluation_finished:#{cell_id}", %{code_error: metadata.code_error})
|
|> push_event("evaluation_finished:#{cell_id}", %{code_error: metadata.code_error})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp after_operation(
|
||||||
|
socket,
|
||||||
|
_prev_socket,
|
||||||
|
{:smart_cell_started, _client_pid, _cell_id, _delta, _js_view, _editor}
|
||||||
|
) do
|
||||||
|
prune_cell_sources(socket)
|
||||||
|
end
|
||||||
|
|
||||||
defp after_operation(socket, _prev_socket, _operation), do: socket
|
defp after_operation(socket, _prev_socket, _operation), do: socket
|
||||||
|
|
||||||
defp handle_actions(socket, actions) do
|
defp handle_actions(socket, actions) do
|
||||||
Enum.reduce(actions, socket, &handle_action(&2, &1))
|
Enum.reduce(actions, socket, &handle_action(&2, &1))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(socket, {:broadcast_delta, client_pid, cell, delta}) do
|
defp handle_action(socket, {:broadcast_delta, client_pid, cell, tag, delta}) do
|
||||||
if client_pid == self() do
|
if client_pid == self() do
|
||||||
push_event(socket, "cell_acknowledgement:#{cell.id}", %{})
|
push_event(socket, "cell_acknowledgement:#{cell.id}:#{tag}", %{})
|
||||||
else
|
else
|
||||||
push_event(socket, "cell_delta:#{cell.id}", %{delta: Delta.to_compressed(delta)})
|
push_event(socket, "cell_delta:#{cell.id}:#{tag}", %{delta: Delta.to_compressed(delta)})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1416,7 +1416,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
%{
|
%{
|
||||||
id: cell.id,
|
id: cell.id,
|
||||||
type: :markdown,
|
type: :markdown,
|
||||||
source_view: cell_source_view(cell, info)
|
source_view: source_view(cell.source, info.sources.primary)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1426,7 +1426,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
%{
|
%{
|
||||||
id: cell.id,
|
id: cell.id,
|
||||||
type: :code,
|
type: :code,
|
||||||
source_view: cell_source_view(cell, info),
|
source_view: source_view(cell.source, info.sources.primary),
|
||||||
eval: eval_info_to_view(cell, info.eval, data),
|
eval: eval_info_to_view(cell, info.eval, data),
|
||||||
reevaluate_automatically: cell.reevaluate_automatically
|
reevaluate_automatically: cell.reevaluate_automatically
|
||||||
}
|
}
|
||||||
|
|
@ -1438,10 +1438,17 @@ defmodule LivebookWeb.SessionLive do
|
||||||
%{
|
%{
|
||||||
id: cell.id,
|
id: cell.id,
|
||||||
type: :smart,
|
type: :smart,
|
||||||
source_view: cell_source_view(cell, info),
|
source_view: source_view(cell.source, info.sources.primary),
|
||||||
eval: eval_info_to_view(cell, info.eval, data),
|
eval: eval_info_to_view(cell, info.eval, data),
|
||||||
status: info.status,
|
status: info.status,
|
||||||
js_view: cell.js_view
|
js_view: cell.js_view,
|
||||||
|
editor:
|
||||||
|
cell.editor &&
|
||||||
|
%{
|
||||||
|
language: cell.editor.language,
|
||||||
|
placement: cell.editor.placement,
|
||||||
|
source_view: source_view(cell.editor.source, info.sources.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1453,21 +1460,21 @@ defmodule LivebookWeb.SessionLive do
|
||||||
evaluation_time_ms: eval_info.evaluation_time_ms,
|
evaluation_time_ms: eval_info.evaluation_time_ms,
|
||||||
evaluation_start: eval_info.evaluation_start,
|
evaluation_start: eval_info.evaluation_start,
|
||||||
evaluation_number: eval_info.evaluation_number,
|
evaluation_number: eval_info.evaluation_number,
|
||||||
|
evaluation_digest: encode_digest(eval_info.evaluation_digest),
|
||||||
outputs_batch_number: eval_info.outputs_batch_number,
|
outputs_batch_number: eval_info.outputs_batch_number,
|
||||||
# Pass input values relevant to the given cell
|
# Pass input values relevant to the given cell
|
||||||
input_values: input_values_for_cell(cell, data)
|
input_values: input_values_for_cell(cell, data)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp cell_source_view(%{source: :__pruned__}, _info) do
|
defp source_view(:__pruned__, _source_info) do
|
||||||
:__pruned__
|
:__pruned__
|
||||||
end
|
end
|
||||||
|
|
||||||
defp cell_source_view(cell, info) do
|
defp source_view(source, source_info) do
|
||||||
%{
|
%{
|
||||||
source: cell.source,
|
source: source,
|
||||||
revision: info.source.revision,
|
revision: source_info.revision
|
||||||
evaluation_digest: encode_digest(get_in(info, [:eval, :evaluation_digest]))
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1485,10 +1492,10 @@ defmodule LivebookWeb.SessionLive do
|
||||||
# most common ones we only update the relevant parts.
|
# most common ones we only update the relevant parts.
|
||||||
defp update_data_view(data_view, prev_data, data, operation) do
|
defp update_data_view(data_view, prev_data, data, operation) do
|
||||||
case operation do
|
case operation do
|
||||||
{:report_cell_revision, _pid, _cell_id, _revision} ->
|
{:report_cell_revision, _pid, _cell_id, _tag, _revision} ->
|
||||||
data_view
|
data_view
|
||||||
|
|
||||||
{:apply_cell_delta, _pid, _cell_id, _delta, _revision} ->
|
{:apply_cell_delta, _pid, _cell_id, _tag, _delta, _revision} ->
|
||||||
update_dirty_status(data_view, data)
|
update_dirty_status(data_view, data)
|
||||||
|
|
||||||
{:update_smart_cell, _pid, _cell_id, _cell_state, _delta} ->
|
{:update_smart_cell, _pid, _cell_id, _cell_state, _delta} ->
|
||||||
|
|
@ -1540,14 +1547,25 @@ defmodule LivebookWeb.SessionLive do
|
||||||
update_in(
|
update_in(
|
||||||
data.notebook,
|
data.notebook,
|
||||||
&Notebook.update_cells(&1, fn
|
&Notebook.update_cells(&1, fn
|
||||||
%Notebook.Cell.Smart{} = cell -> %{cell | source: :__pruned__, attrs: :__pruned__}
|
%Notebook.Cell.Smart{} = cell ->
|
||||||
%{source: _} = cell -> %{cell | source: :__pruned__}
|
%{cell | source: :__pruned__, attrs: :__pruned__}
|
||||||
cell -> cell
|
|> prune_smart_cell_editor_source()
|
||||||
|
|
||||||
|
%{source: _} = cell ->
|
||||||
|
%{cell | source: :__pruned__}
|
||||||
|
|
||||||
|
cell ->
|
||||||
|
cell
|
||||||
end)
|
end)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp prune_smart_cell_editor_source(%{editor: %{source: _}} = cell),
|
||||||
|
do: put_in(cell.editor.source, :__pruned__)
|
||||||
|
|
||||||
|
defp prune_smart_cell_editor_source(cell), do: cell
|
||||||
|
|
||||||
# Changes that affect only a single cell are still likely to
|
# Changes that affect only a single cell are still likely to
|
||||||
# have impact on dirtiness, so we need to always mirror it
|
# have impact on dirtiness, so we need to always mirror it
|
||||||
defp update_dirty_status(data_view, data) do
|
defp update_dirty_status(data_view, data) do
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,6 @@
|
||||||
defmodule LivebookWeb.SessionLive.CellComponent do
|
defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
use LivebookWeb, :live_component
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(socket) do
|
|
||||||
{:ok, assign(socket, initialized: false)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def update(assigns, socket) do
|
|
||||||
socket = assign(socket, assigns)
|
|
||||||
|
|
||||||
socket =
|
|
||||||
if not connected?(socket) or socket.assigns.initialized do
|
|
||||||
socket
|
|
||||||
else
|
|
||||||
%{id: id, source_view: source_view} = socket.assigns.cell_view
|
|
||||||
|
|
||||||
socket
|
|
||||||
|> push_event("cell_init:#{id}", source_view)
|
|
||||||
|> assign(initialized: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -34,7 +11,8 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
data-cell-id={@cell_view.id}
|
data-cell-id={@cell_view.id}
|
||||||
data-focusable-id={@cell_view.id}
|
data-focusable-id={@cell_view.id}
|
||||||
data-type={@cell_view.type}
|
data-type={@cell_view.type}
|
||||||
data-session-path={Routes.session_path(@socket, :page, @session_id)}>
|
data-session-path={Routes.session_path(@socket, :page, @session_id)}
|
||||||
|
data-evaluation-digest={get_in(@cell_view, [:eval, :evaluation_digest])}>
|
||||||
<%= render_cell(assigns) %>
|
<%= render_cell(assigns) %>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
@ -54,13 +32,18 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
</.cell_actions>
|
</.cell_actions>
|
||||||
<.cell_body>
|
<.cell_body>
|
||||||
<div class="pb-4" data-element="editor-box">
|
<div class="pb-4" data-element="editor-box">
|
||||||
<.editor cell_view={@cell_view} />
|
<.live_component module={LivebookWeb.SessionLive.CellEditorComponent}
|
||||||
|
id={"#{@cell_view.id}-primary"}
|
||||||
|
cell_id={@cell_view.id}
|
||||||
|
tag="primary"
|
||||||
|
source_view={@cell_view.source_view}
|
||||||
|
language="markdown" />
|
||||||
</div>
|
</div>
|
||||||
<div class="markdown"
|
<div class="markdown"
|
||||||
data-element="markdown-container"
|
data-element="markdown-container"
|
||||||
id={"markdown-container-#{@cell_view.id}"}
|
id={"markdown-container-#{@cell_view.id}"}
|
||||||
phx-update="ignore">
|
phx-update="ignore">
|
||||||
<.content_placeholder bg_class="bg-gray-200" empty={empty?(@cell_view.source_view)} />
|
<.content_skeleton empty={empty?(@cell_view.source_view)} />
|
||||||
</div>
|
</div>
|
||||||
</.cell_body>
|
</.cell_body>
|
||||||
"""
|
"""
|
||||||
|
|
@ -89,7 +72,13 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
</.cell_actions>
|
</.cell_actions>
|
||||||
<.cell_body>
|
<.cell_body>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<.editor cell_view={@cell_view} />
|
<.live_component module={LivebookWeb.SessionLive.CellEditorComponent}
|
||||||
|
id={"#{@cell_view.id}-primary"}
|
||||||
|
cell_id={@cell_view.id}
|
||||||
|
tag="primary"
|
||||||
|
source_view={@cell_view.source_view}
|
||||||
|
language="elixir"
|
||||||
|
intellisense />
|
||||||
<div class="absolute bottom-2 right-2">
|
<div class="absolute bottom-2 right-2">
|
||||||
<.cell_status cell_view={@cell_view} />
|
<.cell_status cell_view={@cell_view} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -129,10 +118,20 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
<div data-element="ui-box">
|
<div data-element="ui-box">
|
||||||
<%= case @cell_view.status do %>
|
<%= case @cell_view.status do %>
|
||||||
<% :started -> %>
|
<% :started -> %>
|
||||||
|
<div class={"flex #{if(@cell_view.editor && @cell_view.editor.placement == :top, do: "flex-col-reverse", else: "flex-col")}"}>
|
||||||
<.live_component module={LivebookWeb.JSViewComponent}
|
<.live_component module={LivebookWeb.JSViewComponent}
|
||||||
id={@cell_view.id}
|
id={@cell_view.id}
|
||||||
js_view={@cell_view.js_view}
|
js_view={@cell_view.js_view}
|
||||||
session_id={@session_id} />
|
session_id={@session_id} />
|
||||||
|
<%= if @cell_view.editor do %>
|
||||||
|
<.live_component module={LivebookWeb.SessionLive.CellEditorComponent}
|
||||||
|
id={"#{@cell_view.id}-secondary"}
|
||||||
|
cell_id={@cell_view.id}
|
||||||
|
tag="secondary"
|
||||||
|
source_view={@cell_view.editor.source_view}
|
||||||
|
language={@cell_view.editor.language} />
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% :dead -> %>
|
<% :dead -> %>
|
||||||
<div class="p-4 bg-gray-100 text-sm text-gray-500 font-medium rounded-lg">
|
<div class="p-4 bg-gray-100 text-sm text-gray-500 font-medium rounded-lg">
|
||||||
|
|
@ -141,12 +140,19 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
|
|
||||||
<% :starting -> %>
|
<% :starting -> %>
|
||||||
<div class="delay-200">
|
<div class="delay-200">
|
||||||
<.content_placeholder bg_class="bg-gray-200" empty={false} />
|
<.content_skeleton empty={false} />
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div data-element="editor-box">
|
<div data-element="editor-box">
|
||||||
<.editor cell_view={@cell_view} />
|
<.live_component module={LivebookWeb.SessionLive.CellEditorComponent}
|
||||||
|
id={"#{@cell_view.id}-primary"}
|
||||||
|
cell_id={@cell_view.id}
|
||||||
|
tag="primary"
|
||||||
|
source_view={@cell_view.source_view}
|
||||||
|
language="elixir"
|
||||||
|
intellisense
|
||||||
|
read_only />
|
||||||
</div>
|
</div>
|
||||||
<div data-element="cell-status-container">
|
<div data-element="cell-status-container">
|
||||||
<.cell_status cell_view={@cell_view} />
|
<.cell_status cell_view={@cell_view} />
|
||||||
|
|
@ -373,18 +379,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp editor(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div id={"editor-#{@cell_view.id}"} phx-update="ignore">
|
|
||||||
<div class="py-3 rounded-lg bg-editor" data-element="editor-container">
|
|
||||||
<div class="px-8">
|
|
||||||
<.content_placeholder bg_class="bg-gray-500" empty={empty?(@cell_view.source_view)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp evaluation_outputs(assigns) do
|
defp evaluation_outputs(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex flex-col"
|
<div class="flex flex-col"
|
||||||
|
|
@ -403,26 +397,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
# The whole page has to load and then hooks are mounted.
|
|
||||||
# There may be a tiny delay before the markdown is rendered
|
|
||||||
# or editors are mounted, so show neat placeholders immediately.
|
|
||||||
|
|
||||||
defp content_placeholder(assigns) do
|
|
||||||
~H"""
|
|
||||||
<%= if @empty do %>
|
|
||||||
<div class="h-4"></div>
|
|
||||||
<% else %>
|
|
||||||
<div class="max-w-2xl w-full animate-pulse">
|
|
||||||
<div class="flex-1 space-y-4">
|
|
||||||
<div class={"#{@bg_class} h-4 rounded-lg w-3/4"}></div>
|
|
||||||
<div class={"#{@bg_class} h-4 rounded-lg"}></div>
|
|
||||||
<div class={"#{@bg_class} h-4 rounded-lg w-5/6"}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp empty?(%{source: ""} = _source_view), do: true
|
defp empty?(%{source: ""} = _source_view), do: true
|
||||||
defp empty?(_source_view), do: false
|
defp empty?(_source_view), do: false
|
||||||
|
|
||||||
|
|
|
||||||
56
lib/livebook_web/live/session_live/cell_editor_component.ex
Normal file
56
lib/livebook_web/live/session_live/cell_editor_component.ex
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
defmodule LivebookWeb.SessionLive.CellEditorComponent do
|
||||||
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(socket) do
|
||||||
|
{:ok, assign(socket, initialized: false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(assigns)
|
||||||
|
|> assign_new(:intellisense, fn -> false end)
|
||||||
|
|> assign_new(:read_only, fn -> false end)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
if not connected?(socket) or socket.assigns.initialized do
|
||||||
|
socket
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
|> push_event(
|
||||||
|
"cell_editor_init:#{socket.assigns.cell_id}:#{socket.assigns.tag}",
|
||||||
|
%{
|
||||||
|
source_view: socket.assigns.source_view,
|
||||||
|
language: socket.assigns.language,
|
||||||
|
intellisense: socket.assigns.intellisense,
|
||||||
|
read_only: socket.assigns.read_only
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> assign(initialized: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div id={"cell-editor-#{@id}"}
|
||||||
|
phx-update="ignore"
|
||||||
|
phx-hook="CellEditor"
|
||||||
|
data-cell-id={@cell_id}
|
||||||
|
data-tag={@tag}>
|
||||||
|
<div class="py-3 rounded-lg bg-editor" data-element="editor-container">
|
||||||
|
<div class="px-8">
|
||||||
|
<.content_skeleton bg_class="bg-gray-500" empty={empty?(@source_view)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp empty?(%{source: ""} = _source_view), do: true
|
||||||
|
defp empty?(_source_view), do: false
|
||||||
|
end
|
||||||
|
|
@ -216,8 +216,9 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
||||||
|
|
||||||
{:ok, pid,
|
{:ok, pid,
|
||||||
%{
|
%{
|
||||||
js_view: %{ref: info.ref, pid: pid, assets: %{}},
|
|
||||||
source: "source",
|
source: "source",
|
||||||
|
js_view: %{ref: info.ref, pid: pid, assets: %{}},
|
||||||
|
editor: nil,
|
||||||
scan_binding: fn pid, _binding, _env -> send(pid, :scan_binding_ping) end,
|
scan_binding: fn pid, _binding, _env -> send(pid, :scan_binding_ping) end,
|
||||||
scan_eval_result: fn pid, _result -> send(pid, :scan_eval_result_ping) end
|
scan_eval_result: fn pid, _result -> send(pid, :scan_eval_result_ping) end
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -421,7 +421,9 @@ defmodule Livebook.Session.DataTest do
|
||||||
|
|
||||||
assert {:ok,
|
assert {:ok,
|
||||||
%{
|
%{
|
||||||
cell_infos: %{"c1" => %{source: %{revision_by_client_pid: %{^client_pid => 0}}}}
|
cell_infos: %{
|
||||||
|
"c1" => %{sources: %{primary: %{revision_by_client_pid: %{^client_pid => 0}}}}
|
||||||
|
}
|
||||||
}, []} = Data.apply_operation(data, operation)
|
}, []} = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -461,7 +463,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
|
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
|
||||||
{:set_runtime, self(), NoopRuntime.new()},
|
{:set_runtime, self(), NoopRuntime.new()},
|
||||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||||
{:smart_cell_started, self(), "c2", Delta.new(), %{}}
|
{:smart_cell_started, self(), "c2", Delta.new(), %{}, nil}
|
||||||
])
|
])
|
||||||
|
|
||||||
operation = {:insert_cell, self(), "s1", 0, :code, "c3", %{}}
|
operation = {:insert_cell, self(), "s1", 0, :code, "c3", %{}}
|
||||||
|
|
@ -813,7 +815,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:set_runtime, self(), NoopRuntime.new()},
|
{:set_runtime, self(), NoopRuntime.new()},
|
||||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||||
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}},
|
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}},
|
||||||
{:smart_cell_started, self(), "c1", Delta.new(), %{}}
|
{:smart_cell_started, self(), "c1", Delta.new(), %{}, nil}
|
||||||
])
|
])
|
||||||
|
|
||||||
operation = {:delete_cell, self(), "c1"}
|
operation = {:delete_cell, self(), "c1"}
|
||||||
|
|
@ -830,7 +832,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
|
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
|
||||||
{:set_runtime, self(), NoopRuntime.new()},
|
{:set_runtime, self(), NoopRuntime.new()},
|
||||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||||
{:smart_cell_started, self(), "c2", Delta.new(), %{}},
|
{:smart_cell_started, self(), "c2", Delta.new(), %{}, nil},
|
||||||
{:queue_cells_evaluation, self(), ["c1"]},
|
{:queue_cells_evaluation, self(), ["c1"]},
|
||||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||||
])
|
])
|
||||||
|
|
@ -2373,7 +2375,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
|
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
|
||||||
{:set_runtime, self(), NoopRuntime.new()},
|
{:set_runtime, self(), NoopRuntime.new()},
|
||||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||||
{:smart_cell_started, self(), "c2", Delta.new(), %{}},
|
{:smart_cell_started, self(), "c2", Delta.new(), %{}, nil},
|
||||||
{:queue_cells_evaluation, self(), ["c1"]}
|
{:queue_cells_evaluation, self(), ["c1"]}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
@ -2683,7 +2685,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}}
|
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}}
|
||||||
])
|
])
|
||||||
|
|
||||||
operation = {:smart_cell_started, self(), "c1", Delta.new(), %{}}
|
operation = {:smart_cell_started, self(), "c1", Delta.new(), %{}, nil}
|
||||||
|
|
||||||
assert :error = Data.apply_operation(data, operation)
|
assert :error = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
@ -2699,7 +2701,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
|
|
||||||
delta = Delta.new() |> Delta.insert("content")
|
delta = Delta.new() |> Delta.insert("content")
|
||||||
|
|
||||||
operation = {:smart_cell_started, self(), "c1", delta, %{}}
|
operation = {:smart_cell_started, self(), "c1", delta, %{}, nil}
|
||||||
|
|
||||||
assert {:ok, %{cell_infos: %{"c1" => %{status: :started}}}, _actions} =
|
assert {:ok, %{cell_infos: %{"c1" => %{status: :started}}}, _actions} =
|
||||||
Data.apply_operation(data, operation)
|
Data.apply_operation(data, operation)
|
||||||
|
|
@ -2718,13 +2720,13 @@ defmodule Livebook.Session.DataTest do
|
||||||
|
|
||||||
delta = Delta.new() |> Delta.insert("content")
|
delta = Delta.new() |> Delta.insert("content")
|
||||||
|
|
||||||
operation = {:smart_cell_started, client_pid, "c1", delta, %{}}
|
operation = {:smart_cell_started, client_pid, "c1", delta, %{}, nil}
|
||||||
|
|
||||||
assert {:ok,
|
assert {:ok,
|
||||||
%{
|
%{
|
||||||
notebook: %{sections: [%{cells: [%{id: "c1", source: "content"}]}]}
|
notebook: %{sections: [%{cells: [%{id: "c1", source: "content"}]}]}
|
||||||
},
|
},
|
||||||
[{:broadcast_delta, ^client_pid, _cell, ^delta}]} =
|
[{:broadcast_delta, ^client_pid, _cell, :primary, ^delta}]} =
|
||||||
Data.apply_operation(data, operation)
|
Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -2741,7 +2743,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:set_runtime, self(), NoopRuntime.new()},
|
{:set_runtime, self(), NoopRuntime.new()},
|
||||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||||
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}},
|
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}},
|
||||||
{:smart_cell_started, self(), "c1", delta1, %{}}
|
{:smart_cell_started, self(), "c1", delta1, %{}, nil}
|
||||||
])
|
])
|
||||||
|
|
||||||
attrs = %{"text" => "content!"}
|
attrs = %{"text" => "content!"}
|
||||||
|
|
@ -2754,7 +2756,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
sections: [%{cells: [%{id: "c1", source: "content!", attrs: ^attrs}]}]
|
sections: [%{cells: [%{id: "c1", source: "content!", attrs: ^attrs}]}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[{:broadcast_delta, ^client_pid, _cell, ^delta2}]} =
|
[{:broadcast_delta, ^client_pid, _cell, :primary, ^delta2}]} =
|
||||||
Data.apply_operation(data, operation)
|
Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -2888,7 +2890,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:client_join, client1_pid, user},
|
{:client_join, client1_pid, user},
|
||||||
{:insert_section, self(), 0, "s1"},
|
{:insert_section, self(), 0, "s1"},
|
||||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
||||||
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
{:apply_cell_delta, client1_pid, "c1", :primary, delta1, 1}
|
||||||
])
|
])
|
||||||
|
|
||||||
client2_pid = IEx.Helpers.pid(0, 0, 1)
|
client2_pid = IEx.Helpers.pid(0, 0, 1)
|
||||||
|
|
@ -2896,7 +2898,9 @@ defmodule Livebook.Session.DataTest do
|
||||||
|
|
||||||
assert {:ok,
|
assert {:ok,
|
||||||
%{
|
%{
|
||||||
cell_infos: %{"c1" => %{source: %{revision_by_client_pid: %{^client2_pid => 1}}}}
|
cell_infos: %{
|
||||||
|
"c1" => %{sources: %{primary: %{revision_by_client_pid: %{^client2_pid => 1}}}}
|
||||||
|
}
|
||||||
}, _} = Data.apply_operation(data, operation)
|
}, _} = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -2961,7 +2965,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:client_join, client2_pid, User.new()},
|
{:client_join, client2_pid, User.new()},
|
||||||
{:insert_section, self(), 0, "s1"},
|
{:insert_section, self(), 0, "s1"},
|
||||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
||||||
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
{:apply_cell_delta, client1_pid, "c1", :primary, delta1, 1}
|
||||||
])
|
])
|
||||||
|
|
||||||
operation = {:client_leave, client2_pid}
|
operation = {:client_leave, client2_pid}
|
||||||
|
|
@ -2969,7 +2973,11 @@ defmodule Livebook.Session.DataTest do
|
||||||
assert {:ok,
|
assert {:ok,
|
||||||
%{
|
%{
|
||||||
cell_infos: %{
|
cell_infos: %{
|
||||||
"c1" => %{source: %{deltas: [], revision_by_client_pid: revision_by_client_pid}}
|
"c1" => %{
|
||||||
|
sources: %{
|
||||||
|
primary: %{deltas: [], revision_by_client_pid: revision_by_client_pid}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, _} = Data.apply_operation(data, operation)
|
}, _} = Data.apply_operation(data, operation)
|
||||||
|
|
||||||
|
|
@ -3008,7 +3016,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:client_join, self(), User.new()}
|
{:client_join, self(), User.new()}
|
||||||
])
|
])
|
||||||
|
|
||||||
operation = {:apply_cell_delta, self(), "nonexistent", Delta.new(), 1}
|
operation = {:apply_cell_delta, self(), "nonexistent", :primary, Delta.new(), 1}
|
||||||
assert :error = Data.apply_operation(data, operation)
|
assert :error = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -3020,7 +3028,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
])
|
])
|
||||||
|
|
||||||
delta = Delta.new() |> Delta.insert("cats")
|
delta = Delta.new() |> Delta.insert("cats")
|
||||||
operation = {:apply_cell_delta, self(), "c1", delta, 1}
|
operation = {:apply_cell_delta, self(), "c1", :primary, delta, 1}
|
||||||
assert :error = Data.apply_operation(data, operation)
|
assert :error = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -3033,7 +3041,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
])
|
])
|
||||||
|
|
||||||
delta = Delta.new() |> Delta.insert("cats")
|
delta = Delta.new() |> Delta.insert("cats")
|
||||||
operation = {:apply_cell_delta, self(), "c1", delta, 5}
|
operation = {:apply_cell_delta, self(), "c1", :primary, delta, 5}
|
||||||
|
|
||||||
assert :error = Data.apply_operation(data, operation)
|
assert :error = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
@ -3047,7 +3055,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
])
|
])
|
||||||
|
|
||||||
delta = Delta.new() |> Delta.insert("cats")
|
delta = Delta.new() |> Delta.insert("cats")
|
||||||
operation = {:apply_cell_delta, self(), "c1", delta, 1}
|
operation = {:apply_cell_delta, self(), "c1", :primary, delta, 1}
|
||||||
|
|
||||||
assert {:ok,
|
assert {:ok,
|
||||||
%{
|
%{
|
||||||
|
|
@ -3056,7 +3064,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
%{cells: [%{source: "cats"}]}
|
%{cells: [%{source: "cats"}]}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
cell_infos: %{"c1" => %{source: %{revision: 1}}}
|
cell_infos: %{"c1" => %{sources: %{primary: %{revision: 1}}}}
|
||||||
}, _actions} = Data.apply_operation(data, operation)
|
}, _actions} = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -3072,11 +3080,11 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:client_join, client2_pid, User.new()},
|
{:client_join, client2_pid, User.new()},
|
||||||
{:insert_section, self(), 0, "s1"},
|
{:insert_section, self(), 0, "s1"},
|
||||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
||||||
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
{:apply_cell_delta, client1_pid, "c1", :primary, delta1, 1}
|
||||||
])
|
])
|
||||||
|
|
||||||
delta2 = Delta.new() |> Delta.insert("tea")
|
delta2 = Delta.new() |> Delta.insert("tea")
|
||||||
operation = {:apply_cell_delta, client2_pid, "c1", delta2, 1}
|
operation = {:apply_cell_delta, client2_pid, "c1", :primary, delta2, 1}
|
||||||
|
|
||||||
assert {:ok,
|
assert {:ok,
|
||||||
%{
|
%{
|
||||||
|
|
@ -3085,7 +3093,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
%{cells: [%{source: "catstea"}]}
|
%{cells: [%{source: "catstea"}]}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
cell_infos: %{"c1" => %{source: %{revision: 2}}}
|
cell_infos: %{"c1" => %{sources: %{primary: %{revision: 2}}}}
|
||||||
}, _} = Data.apply_operation(data, operation)
|
}, _} = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -3101,15 +3109,16 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:client_join, client2_pid, User.new()},
|
{:client_join, client2_pid, User.new()},
|
||||||
{:insert_section, self(), 0, "s1"},
|
{:insert_section, self(), 0, "s1"},
|
||||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
||||||
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
{:apply_cell_delta, client1_pid, "c1", :primary, delta1, 1}
|
||||||
])
|
])
|
||||||
|
|
||||||
delta2 = Delta.new() |> Delta.insert("tea")
|
delta2 = Delta.new() |> Delta.insert("tea")
|
||||||
operation = {:apply_cell_delta, client2_pid, "c1", delta2, 1}
|
operation = {:apply_cell_delta, client2_pid, "c1", :primary, delta2, 1}
|
||||||
|
|
||||||
transformed_delta2 = Delta.new() |> Delta.retain(4) |> Delta.insert("tea")
|
transformed_delta2 = Delta.new() |> Delta.retain(4) |> Delta.insert("tea")
|
||||||
|
|
||||||
assert {:ok, _data, [{:broadcast_delta, ^client2_pid, _cell, ^transformed_delta2}]} =
|
assert {:ok, _data,
|
||||||
|
[{:broadcast_delta, ^client2_pid, _cell, :primary, ^transformed_delta2}]} =
|
||||||
Data.apply_operation(data, operation)
|
Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -3124,11 +3133,11 @@ defmodule Livebook.Session.DataTest do
|
||||||
])
|
])
|
||||||
|
|
||||||
delta = Delta.new() |> Delta.insert("cats")
|
delta = Delta.new() |> Delta.insert("cats")
|
||||||
operation = {:apply_cell_delta, client_pid, "c1", delta, 1}
|
operation = {:apply_cell_delta, client_pid, "c1", :primary, delta, 1}
|
||||||
|
|
||||||
assert {:ok,
|
assert {:ok,
|
||||||
%{
|
%{
|
||||||
cell_infos: %{"c1" => %{source: %{deltas: []}}}
|
cell_infos: %{"c1" => %{sources: %{primary: %{deltas: []}}}}
|
||||||
}, _} = Data.apply_operation(data, operation)
|
}, _} = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -3145,13 +3154,39 @@ defmodule Livebook.Session.DataTest do
|
||||||
])
|
])
|
||||||
|
|
||||||
delta = Delta.new() |> Delta.insert("cats")
|
delta = Delta.new() |> Delta.insert("cats")
|
||||||
operation = {:apply_cell_delta, client1_pid, "c1", delta, 1}
|
operation = {:apply_cell_delta, client1_pid, "c1", :primary, delta, 1}
|
||||||
|
|
||||||
assert {:ok,
|
assert {:ok,
|
||||||
%{
|
%{
|
||||||
cell_infos: %{"c1" => %{source: %{deltas: [^delta]}}}
|
cell_infos: %{"c1" => %{sources: %{primary: %{deltas: [^delta]}}}}
|
||||||
}, _} = Data.apply_operation(data, operation)
|
}, _} = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "updates smart cell editor source given a secondary source delta" do
|
||||||
|
data =
|
||||||
|
data_after_operations!([
|
||||||
|
{:client_join, self(), User.new()},
|
||||||
|
{:insert_section, self(), 0, "s1"},
|
||||||
|
{:insert_cell, self(), "s1", 1, :smart, "c1", %{kind: "text"}},
|
||||||
|
{:set_runtime, self(), NoopRuntime.new()},
|
||||||
|
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||||
|
{:smart_cell_started, self(), "c1", Delta.new(), %{},
|
||||||
|
%{language: "text", placement: :bottom, source: ""}}
|
||||||
|
])
|
||||||
|
|
||||||
|
delta = Delta.new() |> Delta.insert("cats")
|
||||||
|
operation = {:apply_cell_delta, self(), "c1", :secondary, delta, 1}
|
||||||
|
|
||||||
|
assert {:ok,
|
||||||
|
%{
|
||||||
|
notebook: %{
|
||||||
|
sections: [
|
||||||
|
%{cells: [%{editor: %{source: "cats"}}]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
cell_infos: %{"c1" => %{sources: %{secondary: %{revision: 1}}}}
|
||||||
|
}, _actions} = Data.apply_operation(data, operation)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "apply_operation/2 given :report_cell_revision" do
|
describe "apply_operation/2 given :report_cell_revision" do
|
||||||
|
|
@ -3161,7 +3196,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:client_join, self(), User.new()}
|
{:client_join, self(), User.new()}
|
||||||
])
|
])
|
||||||
|
|
||||||
operation = {:report_cell_revision, self(), "nonexistent", 1}
|
operation = {:report_cell_revision, self(), "nonexistent", :primary, 1}
|
||||||
assert :error = Data.apply_operation(data, operation)
|
assert :error = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -3174,10 +3209,10 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:client_join, client1_pid, User.new()},
|
{:client_join, client1_pid, User.new()},
|
||||||
{:insert_section, self(), 0, "s1"},
|
{:insert_section, self(), 0, "s1"},
|
||||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
||||||
{:apply_cell_delta, client1_pid, "c1", Delta.new(insert: "cats"), 1}
|
{:apply_cell_delta, client1_pid, "c1", :primary, Delta.new(insert: "cats"), 1}
|
||||||
])
|
])
|
||||||
|
|
||||||
operation = {:report_cell_revision, client2_pid, "c1", 1}
|
operation = {:report_cell_revision, client2_pid, "c1", :primary, 1}
|
||||||
assert :error = Data.apply_operation(data, operation)
|
assert :error = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -3189,7 +3224,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}}
|
{:insert_cell, self(), "s1", 0, :code, "c1", %{}}
|
||||||
])
|
])
|
||||||
|
|
||||||
operation = {:report_cell_revision, self(), "c1", 1}
|
operation = {:report_cell_revision, self(), "c1", :primary, 1}
|
||||||
assert :error = Data.apply_operation(data, operation)
|
assert :error = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -3205,21 +3240,23 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:client_join, client2_pid, User.new()},
|
{:client_join, client2_pid, User.new()},
|
||||||
{:insert_section, self(), 0, "s1"},
|
{:insert_section, self(), 0, "s1"},
|
||||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
||||||
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
{:apply_cell_delta, client1_pid, "c1", :primary, delta1, 1}
|
||||||
])
|
])
|
||||||
|
|
||||||
operation = {:report_cell_revision, client2_pid, "c1", 1}
|
operation = {:report_cell_revision, client2_pid, "c1", :primary, 1}
|
||||||
|
|
||||||
assert {:ok,
|
assert {:ok,
|
||||||
%{
|
%{
|
||||||
cell_infos: %{
|
cell_infos: %{
|
||||||
"c1" => %{
|
"c1" => %{
|
||||||
source: %{
|
sources: %{
|
||||||
|
primary: %{
|
||||||
deltas: [],
|
deltas: [],
|
||||||
revision_by_client_pid: %{^client1_pid => 1, ^client2_pid => 1}
|
revision_by_client_pid: %{^client1_pid => 1, ^client2_pid => 1}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, _} = Data.apply_operation(data, operation)
|
}, _} = Data.apply_operation(data, operation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -3507,7 +3544,7 @@ defmodule Livebook.Session.DataTest do
|
||||||
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
|
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
|
||||||
# Modify cell 2
|
# Modify cell 2
|
||||||
{:client_join, self(), User.new()},
|
{:client_join, self(), User.new()},
|
||||||
{:apply_cell_delta, self(), "c2", Delta.new() |> Delta.insert("cats"), 1}
|
{:apply_cell_delta, self(), "c2", :primary, Delta.new() |> Delta.insert("cats"), 1}
|
||||||
])
|
])
|
||||||
|
|
||||||
assert Data.cell_ids_for_full_evaluation(data, []) |> Enum.sort() == ["c2", "c3"]
|
assert Data.cell_ids_for_full_evaluation(data, []) |> Enum.sort() == ["c2", "c3"]
|
||||||
|
|
|
||||||
|
|
@ -190,8 +190,10 @@ defmodule Livebook.SessionTest do
|
||||||
delta = Delta.new() |> Delta.insert("cats")
|
delta = Delta.new() |> Delta.insert("cats")
|
||||||
revision = 1
|
revision = 1
|
||||||
|
|
||||||
Session.apply_cell_delta(session.pid, cell_id, delta, revision)
|
Session.apply_cell_delta(session.pid, cell_id, :primary, delta, revision)
|
||||||
assert_receive {:operation, {:apply_cell_delta, ^pid, ^cell_id, ^delta, ^revision}}
|
|
||||||
|
assert_receive {:operation,
|
||||||
|
{:apply_cell_delta, ^pid, ^cell_id, :primary, ^delta, ^revision}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -203,8 +205,8 @@ defmodule Livebook.SessionTest do
|
||||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||||
revision = 1
|
revision = 1
|
||||||
|
|
||||||
Session.report_cell_revision(session.pid, cell_id, revision)
|
Session.report_cell_revision(session.pid, cell_id, :primary, revision)
|
||||||
assert_receive {:operation, {:report_cell_revision, ^pid, ^cell_id, ^revision}}
|
assert_receive {:operation, {:report_cell_revision, ^pid, ^cell_id, :primary, ^revision}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -561,14 +563,7 @@ defmodule Livebook.SessionTest do
|
||||||
describe "smart cells" do
|
describe "smart cells" do
|
||||||
test "notifies subcribers when a smart cell starts and passes source diff as delta" do
|
test "notifies subcribers when a smart cell starts and passes source diff as delta" do
|
||||||
smart_cell = %{Notebook.Cell.new(:smart) | kind: "text", source: "content"}
|
smart_cell = %{Notebook.Cell.new(:smart) | kind: "text", source: "content"}
|
||||||
|
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
||||||
notebook = %{
|
|
||||||
Notebook.new()
|
|
||||||
| sections: [
|
|
||||||
%{Notebook.Section.new() | cells: [smart_cell]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
session = start_session(notebook: notebook)
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
runtime = Livebook.Runtime.NoopRuntime.new()
|
runtime = Livebook.Runtime.NoopRuntime.new()
|
||||||
|
|
@ -580,13 +575,44 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
send(
|
send(
|
||||||
session.pid,
|
session.pid,
|
||||||
{:runtime_smart_cell_started, smart_cell.id, %{source: "content!", js_view: %{}}}
|
{:runtime_smart_cell_started, smart_cell.id,
|
||||||
|
%{source: "content!", js_view: %{}, editor: nil}}
|
||||||
)
|
)
|
||||||
|
|
||||||
delta = Delta.new() |> Delta.retain(7) |> Delta.insert("!")
|
delta = Delta.new() |> Delta.retain(7) |> Delta.insert("!")
|
||||||
cell_id = smart_cell.id
|
cell_id = smart_cell.id
|
||||||
|
|
||||||
assert_receive {:operation, {:smart_cell_started, _, ^cell_id, ^delta, %{}}}
|
assert_receive {:operation, {:smart_cell_started, _, ^cell_id, ^delta, %{}, nil}}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sends an event to the smart cell server when the editor source changes" do
|
||||||
|
smart_cell = %{Notebook.Cell.new(:smart) | kind: "text", source: ""}
|
||||||
|
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
||||||
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
|
runtime = Livebook.Runtime.NoopRuntime.new()
|
||||||
|
Session.connect_runtime(session.pid, runtime)
|
||||||
|
|
||||||
|
send(session.pid, {:runtime_smart_cell_definitions, [%{kind: "text", name: "Text"}]})
|
||||||
|
|
||||||
|
server_pid = self()
|
||||||
|
|
||||||
|
send(
|
||||||
|
session.pid,
|
||||||
|
{:runtime_smart_cell_started, smart_cell.id,
|
||||||
|
%{
|
||||||
|
source: "content",
|
||||||
|
js_view: %{ref: smart_cell.id, pid: server_pid, assets: %{}},
|
||||||
|
editor: %{language: nil, placement: :bottom, source: "content"}
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
Session.register_client(session.pid, self(), Livebook.Users.User.new())
|
||||||
|
|
||||||
|
delta = Delta.new() |> Delta.retain(7) |> Delta.insert("!")
|
||||||
|
Session.apply_cell_delta(session.pid, smart_cell.id, :secondary, delta, 1)
|
||||||
|
|
||||||
|
assert_receive {:editor_source, "content!"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue