Mount Markdown cell editor lazily (#1102)

This commit is contained in:
Jonatan Kłosko 2022-04-13 12:45:29 +02:00 committed by GitHub
parent a144d3d4fb
commit aafdc12015
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 97 additions and 30 deletions

View file

@ -156,6 +156,18 @@ const Cell = {
this.updateInsertModeAvailability();
if (this.props.type !== "markdown") {
// For markdown cells the editor is mounted lazily when needed,
// for other cells we mount the editor eagerly, however mounting
// is a synchronous operation and is relatively expensive, so we
// defer it to run after the current event handlers
setTimeout(() => {
if (!liveEditor.isMounted()) {
liveEditor.mount();
}
}, 0);
}
if (liveEditor === this.currentEditor()) {
// Once the editor is created, reflect the current insert mode state
this.maybeFocusCurrentEditor(true);

View file

@ -12,8 +12,6 @@ const CellEditor = {
`[data-el-editor-container]`
);
// Remove the content placeholder
editorContainer.firstElementChild.remove();
const editorEl = document.createElement("div");
editorContainer.appendChild(editorEl);
@ -29,6 +27,11 @@ const CellEditor = {
read_only
);
this.liveEditor.onMount(() => {
// Remove the content placeholder
editorContainer.querySelector(`[data-el-skeleton]`).remove();
});
this.el.dispatchEvent(
new CustomEvent("lb:cell:editor_created", {
detail: { tag: this.props.tag, liveEditor: this.liveEditor },

View file

@ -28,29 +28,43 @@ class LiveEditor {
this.language = language;
this.intellisense = intellisense;
this.readOnly = readOnly;
this._onMount = [];
this._onChange = [];
this._onBlur = [];
this._onCursorSelectionChange = [];
this._remoteUserByClientPid = {};
const serverAdapter = new HookServerAdapter(hook, cellId, tag);
this.editorClient = new EditorClient(serverAdapter, revision);
this.editorClient.onDelta((delta) => {
this.source = delta.applyToString(this.source);
this._onChange.forEach((callback) => callback(this.source));
});
}
/**
* Checks if an editor instance has been mounted in the DOM.
*/
isMounted() {
return !!this.editor;
}
/**
* Mounts and configures an editor instance in the DOM.
*/
mount() {
if (this.isMounted()) {
throw new Error("The editor is already mounted");
}
this._mountEditor();
if (this.intellisense) {
this._setupIntellisense();
}
const serverAdapter = new HookServerAdapter(hook, cellId, tag);
const editorAdapter = new MonacoEditorAdapter(this.editor);
this.editorClient = new EditorClient(
serverAdapter,
editorAdapter,
revision
);
this.editorClient.onDelta((delta) => {
this.source = delta.applyToString(this.source);
this._onChange.forEach((callback) => callback(this.source));
});
this.editorClient.setEditorAdapter(new MonacoEditorAdapter(this.editor));
this.editor.onDidFocusEditorWidget(() => {
this.editor.updateOptions({ matchBrackets: "always" });
@ -66,6 +80,14 @@ class LiveEditor {
callback(event.selection)
);
});
this._onMount.forEach((callback) => callback());
}
_ensureMounted() {
if (!this.isMounted()) {
this.mount();
}
}
/**
@ -75,6 +97,13 @@ class LiveEditor {
return this.source;
}
/**
* Registers a callback called with the editor is mounted in DOM.
*/
onMount(callback) {
this._onMount.push(callback);
}
/**
* Registers a callback called with a new cell content whenever it changes.
*/
@ -97,16 +126,22 @@ class LiveEditor {
}
focus() {
this._ensureMounted();
this.editor.focus();
}
blur() {
this._ensureMounted();
if (this.editor.hasTextFocus()) {
document.activeElement.blur();
}
}
insert(text) {
this._ensureMounted();
const range = this.editor.getSelection();
this.editor
.getModel()
@ -117,13 +152,15 @@ class LiveEditor {
* Performs necessary cleanup actions.
*/
dispose() {
// Explicitly destroy the editor instance and its text model.
this.editor.dispose();
if (this.isMounted()) {
// Explicitly destroy the editor instance and its text model.
this.editor.dispose();
const model = this.editor.getModel();
const model = this.editor.getModel();
if (model) {
model.dispose();
if (model) {
model.dispose();
}
}
}
@ -131,6 +168,8 @@ class LiveEditor {
* Either adds or moves remote user cursor to the new position.
*/
updateUserSelection(client, selection) {
this._ensureMounted();
if (this._remoteUserByClientPid[client.pid]) {
this._remoteUserByClientPid[client.pid].update(selection);
} else {
@ -147,6 +186,8 @@ class LiveEditor {
* Removes remote user cursor.
*/
removeUserSelection(client) {
this._ensureMounted();
if (this._remoteUserByClientPid[client.pid]) {
this._remoteUserByClientPid[client.pid].dispose();
delete this._remoteUserByClientPid[client.pid];
@ -159,6 +200,8 @@ class LiveEditor {
* To clear an existing marker `null` error is also supported.
*/
setCodeErrorMarker(error) {
this._ensureMounted();
const owner = "livebook.error.syntax";
if (error) {

View file

@ -3,7 +3,7 @@
* which is responsible for controlling client-server communication
* and synchronizing the sent/received changes.
*
* This class takes `serverAdapter` and `editorAdapter` objects
* This class uses `serverAdapter` and `editorAdapter` objects
* that encapsulate the logic relevant for each part.
*
* ## Changes synchronization
@ -21,19 +21,12 @@
* deltas and applied to the editor.
*/
export default class EditorClient {
constructor(serverAdapter, editorAdapter, revision) {
constructor(serverAdapter, revision) {
this.serverAdapter = serverAdapter;
this.editorAdapter = editorAdapter;
this.revision = revision;
this.state = new Synchronized(this);
this._onDelta = null;
this.editorAdapter.onDelta((delta) => {
this._handleClientDelta(delta);
// This delta comes from the editor, so it has already been applied.
this._emitDelta(delta);
});
this.serverAdapter.onDelta((delta) => {
this._handleServerDelta(delta);
});
@ -43,6 +36,22 @@ export default class EditorClient {
});
}
/**
* Plugs in the editor adapter.
*
* The adapter may be set at a later point after initialization, in
* case the editor is mounted lazily.
*/
setEditorAdapter(editorAdapter) {
this.editorAdapter = editorAdapter;
this.editorAdapter.onDelta((delta) => {
this._handleClientDelta(delta);
// This delta comes from the editor, so it has already been applied.
this._emitDelta(delta);
});
}
/**
* Registers a callback called with a every delta applied to the editor.
*
@ -72,7 +81,7 @@ export default class EditorClient {
}
applyDelta(delta) {
this.editorAdapter.applyDelta(delta);
this.editorAdapter && this.editorAdapter.applyDelta(delta);
// This delta comes from the server and we have just applied it to the editor.
this._emitDelta(delta);
}

View file

@ -43,7 +43,7 @@ defmodule LivebookWeb.SessionLive.CellEditorComponent do
data-cell-id={@cell_id}
data-tag={@tag}>
<div class="py-3 rounded-lg bg-editor" data-el-editor-container>
<div class="px-8">
<div class="px-8" data-el-skeleton>
<.content_skeleton bg_class="bg-gray-500" empty={empty?(@source_view)} />
</div>
</div>