From 37e252b90383fefe0daa6ff527929af5ffb19fb0 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Thu, 23 Sep 2021 13:49:59 +0200 Subject: [PATCH 01/18] Use vscode extension API instead of websocket when detected --- frontend/common/PlutoConnection.js | 53 ++++- frontend/common/PlutoHash.js | 5 + frontend/common/VSCodeApi.js | 10 + frontend/components/Editor.js | 333 +++++++++++++++-------------- 4 files changed, 234 insertions(+), 167 deletions(-) create mode 100644 frontend/common/VSCodeApi.js diff --git a/frontend/common/PlutoConnection.js b/frontend/common/PlutoConnection.js index 0cf400e352..3b55e9862a 100644 --- a/frontend/common/PlutoConnection.js +++ b/frontend/common/PlutoConnection.js @@ -1,6 +1,8 @@ import { Promises } from "../common/SetupCellEnvironment.js" import { pack, unpack } from "./MsgPack.js" +import { base64_arraybuffer, decode_base64_to_arraybuffer } from "./PlutoHash.js" import "./Polyfill.js" +import { available as vscode_available, api as vscode_api } from "./VSCodeApi.js" // https://github.com/denysdovhan/wtfjs/issues/61 const different_Infinity_because_js_is_yuck = 2147483646 @@ -179,6 +181,53 @@ const create_ws_connection = (address, { on_message, on_socket_close }, timeout_ }) } +const create_vscode_connection = (address, { on_message, on_socket_close }, timeout_s = 30) => { + return new Promise((resolve, reject) => { + window.addEventListener("message", (event) => { + // we read and deserialize the incoming messages asynchronously + // they arrive in order (WS guarantees this), i.e. this socket.onmessage event gets fired with the message events in the right order + // but some message are read and deserialized much faster than others, because of varying sizes, so _after_ async read & deserialization, messages are no longer guaranteed to be in order + // + // the solution is a task queue, where each task includes the deserialization and the update handler + last_task = last_task.then(async () => { + try { + const raw = event.data // The json-encoded data that the extension sent + console.log("raw", raw) + const buffer = await decode_base64_to_arraybuffer(raw.base64_encoded) + const message = unpack(new Uint8Array(buffer)) + + try { + console.info("message received!", message) + on_message(message) + } catch (process_err) { + console.error("Failed to process message from websocket", process_err, { message }) + // prettier-ignore + alert(`Something went wrong! You might need to refresh the page.\n\nPlease open an issue on https://github.com/fonsp/Pluto.jl with this info:\n\nFailed to process update\n${process_err.message}\n\n${JSON.stringify(event)}`) + } + } catch (unpack_err) { + console.error("Failed to unpack message from websocket", unpack_err, { event }) + + // prettier-ignore + alert(`Something went wrong! You might need to refresh the page.\n\nPlease open an issue on https://github.com/fonsp/Pluto.jl with this info:\n\nFailed to unpack message\n${unpack_err}\n\n${JSON.stringify(event)}`) + } + }) + }) + + const send_encoded = async (message) => { + console.log("Sending message!", message) + const encoded = pack(message) + await vscode_api.postMessage({ base64_encoded: await base64_arraybuffer(encoded) }) + } + + let last_task = Promise.resolve() + + resolve({ + socket: {}, + send: send_encoded, + }) + }) +} + let next_tick_promise = () => { return new Promise((resolve) => setTimeout(resolve, 0)) } @@ -275,7 +324,7 @@ export const create_pluto_connection = async ({ connect_metadata = {}, ws_address = default_ws_address(), }) => { - var ws_connection = null // will be defined later i promise + let ws_connection = null // will be defined later i promise const client = { send: null, session_options: null, @@ -346,7 +395,7 @@ export const create_pluto_connection = async ({ update_url_with_binder_token() try { - ws_connection = await create_ws_connection(String(ws_address), { + ws_connection = await (vscode_available ? create_vscode_connection : create_ws_connection)(String(ws_address), { on_message: (update) => { const by_me = update.initiator_id == client_id const request_id = update.request_id diff --git a/frontend/common/PlutoHash.js b/frontend/common/PlutoHash.js index e0ff80e1c4..412ca3deb7 100644 --- a/frontend/common/PlutoHash.js +++ b/frontend/common/PlutoHash.js @@ -8,6 +8,11 @@ export const base64_arraybuffer = async (/** @type {BufferSource} */ data) => { return base64url.split(",", 2)[1] } +export const decode_base64_to_arraybuffer = async (/** @type {string} */ data) => { + let r = await fetch(`data:;base64,${data}`) + return await r.arrayBuffer() +} + export const hash_arraybuffer = async (/** @type {BufferSource} */ data) => { // @ts-ignore const hashed_buffer = await window.crypto.subtle.digest("SHA-256", data) diff --git a/frontend/common/VSCodeApi.js b/frontend/common/VSCodeApi.js new file mode 100644 index 0000000000..53bca8589e --- /dev/null +++ b/frontend/common/VSCodeApi.js @@ -0,0 +1,10 @@ +// If we are running inside a VS Code WebView, then this exposes the API. + +// @ts-ignore +export const available = window.acquireVsCodeApi != null +export const api = available + ? // @ts-ignore + window.acquireVsCodeApi() + : { + postMessage: console.error, + } diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 4e3aaaa56d..1434eb7c0f 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -1,45 +1,46 @@ -import { html, Component, useState, useEffect, useMemo } from "../imports/Preact.js" -import immer, { applyPatches, produceWithPatches } from "../imports/immer.js" -import _ from "../imports/lodash.js" - -import { create_pluto_connection } from "../common/PlutoConnection.js" -import { init_feedback } from "../common/Feedback.js" -import { serialize_cells, deserialize_cells, detect_deserializer } from "../common/Serialization.js" - -import { FilePicker } from "./FilePicker.js" -import { Preamble } from "./Preamble.js" -import { NotebookMemo as Notebook } from "./Notebook.js" -import { LiveDocs } from "./LiveDocs.js" -import { DropRuler } from "./DropRuler.js" -import { SelectionArea } from "./SelectionArea.js" -import { UndoDelete } from "./UndoDelete.js" -import { SlideControls } from "./SlideControls.js" -import { Scroller } from "./Scroller.js" -import { ExportBanner } from "./ExportBanner.js" -import { PkgPopup } from "./PkgPopup.js" - -import { slice_utf8, length_utf8 } from "../common/UnicodeTools.js" -import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea_or_input } from "../common/KeyboardShortcuts.js" -import { handle_log } from "../common/Logging.js" -import { PlutoContext, PlutoBondsContext, PlutoJSInitializingContext } from "../common/PlutoContext.js" -import { unpack } from "../common/MsgPack.js" -import { useDropHandler } from "./useDropHandler.js" -import { PkgTerminalView } from "./PkgTerminalView.js" -import { start_binder, BinderPhase, count_stat } from "../common/Binder.js" -import { read_Uint8Array_with_progress, FetchProgress } from "./FetchProgress.js" -import { BinderButton } from "./BinderButton.js" -import { slider_server_actions, nothing_actions } from "../common/SliderServerClient.js" -import { ProgressBar } from "./ProgressBar.js" -import { IsolatedCell } from "./Cell.js" - -const default_path = "..." +import { html, Component, useState, useEffect, useMemo } from '../imports/Preact.js' +import immer, { applyPatches, produceWithPatches } from '../imports/immer.js' +import _ from '../imports/lodash.js' + +import { create_pluto_connection } from '../common/PlutoConnection.js' +import { init_feedback } from '../common/Feedback.js' +import { serialize_cells, deserialize_cells, detect_deserializer } from '../common/Serialization.js' + +import { FilePicker } from './FilePicker.js' +import { Preamble } from './Preamble.js' +import { NotebookMemo as Notebook } from './Notebook.js' +import { LiveDocs } from './LiveDocs.js' +import { DropRuler } from './DropRuler.js' +import { SelectionArea } from './SelectionArea.js' +import { UndoDelete } from './UndoDelete.js' +import { SlideControls } from './SlideControls.js' +import { Scroller } from './Scroller.js' +import { ExportBanner } from './ExportBanner.js' +import { PkgPopup } from './PkgPopup.js' + +import { slice_utf8, length_utf8 } from '../common/UnicodeTools.js' +import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea_or_input } from '../common/KeyboardShortcuts.js' +import { handle_log } from '../common/Logging.js' +import { PlutoContext, PlutoBondsContext, PlutoJSInitializingContext } from '../common/PlutoContext.js' +import { unpack } from '../common/MsgPack.js' +import { useDropHandler } from './useDropHandler.js' +import { PkgTerminalView } from './PkgTerminalView.js' +import { start_binder, BinderPhase, count_stat } from '../common/Binder.js' +import { read_Uint8Array_with_progress, FetchProgress } from './FetchProgress.js' +import { BinderButton } from './BinderButton.js' +import { slider_server_actions, nothing_actions } from '../common/SliderServerClient.js' +import { ProgressBar } from './ProgressBar.js' +import { IsolatedCell } from './Cell.js' +import { available as vscode_available } from '../common/VSCodeApi.js' + +const default_path = '...' const DEBUG_DIFFING = false let pending_local_updates = 0 // from our friends at https://stackoverflow.com/a/2117523 // i checked it and it generates Julia-legal UUIDs and that's all we need -SNOF const uuidv4 = () => //@ts-ignore - "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)) + '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)) /** * @typedef {import('../imports/immer').Patch} Patch @@ -48,25 +49,25 @@ const uuidv4 = () => const Main = ({ children }) => { const { handler } = useDropHandler() useEffect(() => { - document.body.addEventListener("drop", handler) - document.body.addEventListener("dragover", handler) - document.body.addEventListener("dragenter", handler) - document.body.addEventListener("dragleave", handler) + document.body.addEventListener('drop', handler) + document.body.addEventListener('dragover', handler) + document.body.addEventListener('dragenter', handler) + document.body.addEventListener('dragleave', handler) return () => { - document.body.removeEventListener("drop", handler) - document.body.removeEventListener("dragover", handler) - document.body.removeEventListener("dragenter", handler) - document.body.removeEventListener("dragleave", handler) + document.body.removeEventListener('drop', handler) + document.body.removeEventListener('dragover', handler) + document.body.removeEventListener('dragenter', handler) + document.body.removeEventListener('dragleave', handler) } }) return html`
${children}
` } const ProcessStatus = { - ready: "ready", - starting: "starting", - no_process: "no_process", - waiting_to_restart: "waiting_to_restart", + ready: 'ready', + starting: 'starting', + no_process: 'no_process', + waiting_to_restart: 'waiting_to_restart', } /** @@ -160,25 +161,25 @@ const first_true_key = (obj) => { * }} */ -const url_logo_big = document.head.querySelector("link[rel='pluto-logo-big']").getAttribute("href") -const url_logo_small = document.head.querySelector("link[rel='pluto-logo-small']").getAttribute("href") +const url_logo_big = document.head.querySelector("link[rel='pluto-logo-big']").getAttribute('href') +const url_logo_small = document.head.querySelector("link[rel='pluto-logo-small']").getAttribute('href') const url_params = new URLSearchParams(window.location.search) const launch_params = { //@ts-ignore - notebook_id: url_params.get("id") ?? window.pluto_notebook_id, + notebook_id: (vscode_available ? null : url_params.get('id')) ?? window.pluto_notebook_id, //@ts-ignore - statefile: url_params.get("statefile") ?? window.pluto_statefile, + statefile: url_params.get('statefile') ?? window.pluto_statefile, //@ts-ignore - notebookfile: url_params.get("notebookfile") ?? window.pluto_notebookfile, + notebookfile: url_params.get('notebookfile') ?? window.pluto_notebookfile, //@ts-ignore - disable_ui: !!(url_params.get("disable_ui") ?? window.pluto_disable_ui), + disable_ui: !!(url_params.get('disable_ui') ?? window.pluto_disable_ui), //@ts-ignore - isolated_cell_ids: url_params.getAll("isolated_cell_id") ?? window.isolated_cell_id, + isolated_cell_ids: url_params.getAll('isolated_cell_id') ?? window.isolated_cell_id, //@ts-ignore - binder_url: url_params.get("binder_url") ?? window.pluto_binder_url, + binder_url: url_params.get('binder_url') ?? window.pluto_binder_url, //@ts-ignore - slider_server_url: url_params.get("slider_server_url") ?? window.pluto_slider_server_url, + slider_server_url: url_params.get('slider_server_url') ?? window.pluto_slider_server_url, } /** @@ -188,9 +189,9 @@ const launch_params = { const initial_notebook = () => ({ notebook_id: launch_params.notebook_id, path: default_path, - shortpath: "", + shortpath: '', in_temp_dir: true, - process_status: "starting", + process_status: 'starting', last_save_time: 0.0, last_hot_reload_time: 0.0, cell_inputs: {}, @@ -260,7 +261,7 @@ export class Editor extends Component { const new_i = i + delta if (new_i >= 0 && new_i < this.state.notebook.cell_order.length) { window.dispatchEvent( - new CustomEvent("cell_focus", { + new CustomEvent('cell_focus', { detail: { cell_id: this.state.notebook.cell_order[new_i], line: line, @@ -283,7 +284,7 @@ export class Editor extends Component { let index - if (typeof index_or_id === "number") { + if (typeof index_or_id === 'number') { index = index_or_id } else { /* if the input is not an integer, try interpreting it as a cell id */ @@ -323,7 +324,7 @@ export class Editor extends Component { notebook.cell_inputs[cell.cell_id] = { ...cell, // Fill the cell with empty code remotely, so it doesn't run unsafe code - code: "", + code: '', } } notebook.cell_order = [ @@ -333,9 +334,9 @@ export class Editor extends Component { ] }) }, - wrap_remote_cell: async (cell_id, block_start = "begin", block_end = "end") => { + wrap_remote_cell: async (cell_id, block_start = 'begin', block_end = 'end') => { const cell = this.state.notebook.cell_inputs[cell_id] - const new_code = `${block_start}\n\t${cell.code.replace(/\n/g, "\n\t")}\n${block_end}` + const new_code = `${block_start}\n\t${cell.code.replace(/\n/g, '\n\t')}\n${block_end}` await this.setStatePromise( immer((state) => { @@ -354,7 +355,7 @@ export class Editor extends Component { const old_code = cell.code const padded_boundaries = [0, ...boundaries] /** @type {Array} */ - const parts = boundaries.map((b, i) => slice_utf8(old_code, padded_boundaries[i], b).trim()).filter((x) => x !== "") + const parts = boundaries.map((b, i) => slice_utf8(old_code, padded_boundaries[i], b).trim()).filter((x) => x !== '') /** @type {Array} */ const cells_to_add = parts.map((code) => { return { @@ -402,7 +403,7 @@ export class Editor extends Component { // }), // } // }) - this.client.send("interrupt_all", {}, { notebook_id: this.state.notebook.notebook_id }, false) + this.client.send('interrupt_all', {}, { notebook_id: this.state.notebook.notebook_id }, false) }, move_remote_cells: (cell_ids, new_index) => { update_notebook((notebook) => { @@ -411,7 +412,7 @@ export class Editor extends Component { notebook.cell_order = [...before, ...cell_ids, ...after] }) }, - add_remote_cell_at: async (index, code = "") => { + add_remote_cell_at: async (index, code = '') => { let id = uuidv4() this.setState({ last_created_cell: id }) await update_notebook((notebook) => { @@ -423,18 +424,18 @@ export class Editor extends Component { } notebook.cell_order = [...notebook.cell_order.slice(0, index), id, ...notebook.cell_order.slice(index, Infinity)] }) - await this.client.send("run_multiple_cells", { cells: [id] }, { notebook_id: this.state.notebook.notebook_id }) + await this.client.send('run_multiple_cells', { cells: [id] }, { notebook_id: this.state.notebook.notebook_id }) return id }, add_remote_cell: async (cell_id, before_or_after, code) => { const index = this.state.notebook.cell_order.indexOf(cell_id) - const delta = before_or_after == "before" ? 0 : 1 + const delta = before_or_after == 'before' ? 0 : 1 return await this.actions.add_remote_cell_at(index + delta, code) }, confirm_delete_multiple: async (verb, cell_ids) => { if (cell_ids.length <= 1 || confirm(`${verb} ${cell_ids.length} cells?`)) { if (cell_ids.some((cell_id) => this.state.notebook.cell_results[cell_id].running || this.state.notebook.cell_results[cell_id].queued)) { - if (confirm("This cell is still running - would you like to interrupt the notebook?")) { + if (confirm('This cell is still running - would you like to interrupt the notebook?')) { this.actions.interrupt_remote(cell_ids[0]) } } else { @@ -453,7 +454,7 @@ export class Editor extends Component { } notebook.cell_order = notebook.cell_order.filter((cell_id) => !cell_ids.includes(cell_id)) }) - await this.client.send("run_multiple_cells", { cells: [] }, { notebook_id: this.state.notebook.notebook_id }) + await this.client.send('run_multiple_cells', { cells: [] }, { notebook_id: this.state.notebook.notebook_id }) } } }, @@ -497,7 +498,7 @@ export class Editor extends Component { } }) ) - await this.client.send("run_multiple_cells", { cells: cell_ids }, { notebook_id: this.state.notebook.notebook_id }) + await this.client.send('run_multiple_cells', { cells: cell_ids }, { notebook_id: this.state.notebook.notebook_id }) } }, /** @@ -518,7 +519,7 @@ export class Editor extends Component { }, reshow_cell: (cell_id, objectid, dim) => { this.client.send( - "reshow_cell", + 'reshow_cell', { objectid: objectid, dim: dim, @@ -530,7 +531,7 @@ export class Editor extends Component { }, write_file: (cell_id, { file, name, type }) => { return this.client.send( - "write_file", + 'write_file', { file, name, type, path: this.state.notebook.path }, { notebook_id: this.state.notebook.notebook_id, @@ -540,7 +541,7 @@ export class Editor extends Component { ) }, get_avaible_versions: async ({ package_name, notebook_id }) => { - const { message } = await this.client.send("nbpkg_available_versions", { package_name: package_name }, { notebook_id: notebook_id }) + const { message } = await this.client.send('nbpkg_available_versions', { package_name: package_name }, { notebook_id: notebook_id }) return message.versions }, } @@ -558,9 +559,9 @@ export class Editor extends Component { // } new_notebook = applyPatches(old_state ?? state.notebook, patches) } catch (exception) { - const failing_path = String(exception).match(".*'(.*)'.*")[1].replace(/\//gi, ".") - const path_value = _.get(this.state.notebook, failing_path, "Not Found") - console.log(String(exception).match(".*'(.*)'.*")[1].replace(/\//gi, "."), failing_path, typeof failing_path) + const failing_path = String(exception).match(".*'(.*)'.*")[1].replace(/\//gi, '.') + const path_value = _.get(this.state.notebook, failing_path, 'Not Found') + console.log(String(exception).match(".*'(.*)'.*")[1].replace(/\//gi, '.'), failing_path, typeof failing_path) // The alert below is not catastrophic: the editor will try to recover. // Deactivating to be user-friendly! // alert(`Ooopsiee.`) @@ -571,16 +572,16 @@ Please report this: https://github.com/fonsp/Pluto.jl/issues adding the info bel failing path: ${failing_path} notebook previous value: ${path_value} patch: ${JSON.stringify( - patches?.find(({ path }) => path.join("") === failing_path), + patches?.find(({ path }) => path.join('') === failing_path), null, 1 )} #######################**************************########################`, exception ) - console.log("Trying to recover: Refetching notebook...") + console.log('Trying to recover: Refetching notebook...') this.client.send( - "reset_shared_state", + 'reset_shared_state', {}, { notebook_id: this.state.notebook.notebook_id, @@ -591,7 +592,7 @@ patch: ${JSON.stringify( } if (DEBUG_DIFFING) { - console.group("Update!") + console.group('Update!') for (let patch of patches) { console.group(`Patch :${patch.op}`) console.log(patch.path) @@ -620,23 +621,23 @@ patch: ${JSON.stringify( if (this.state.notebook.notebook_id === update.notebook_id) { const message = update.message switch (update.type) { - case "notebook_diff": + case 'notebook_diff': if (message?.response?.from_reset) { - console.log("Trying to reset state after failure") + console.log('Trying to reset state after failure') try { apply_notebook_patches(message.patches, initial_notebook()) } catch (exception) { - alert("Oopsie!! please refresh your browser and everything will be alright!") + alert('Oopsie!! please refresh your browser and everything will be alright!') } } else if (message.patches.length !== 0) { apply_notebook_patches(message.patches) } break - case "log": + case 'log': handle_log(message, this.state.notebook.path) break default: - console.error("Received unknown update type!", update) + console.error('Received unknown update type!', update) // alert("Something went wrong 🙈\n Try clearing your browser cache and refreshing the page") break } @@ -652,13 +653,13 @@ patch: ${JSON.stringify( // @ts-ignore window.version_info = this.client.version_info // for debugging - await this.client.send("update_notebook", { updates: [] }, { notebook_id: this.state.notebook.notebook_id }, false) + await this.client.send('update_notebook', { updates: [] }, { notebook_id: this.state.notebook.notebook_id }, false) this.setState({ initializing: false, static_preview: false, binder_phase: this.state.binder_phase == null ? null : BinderPhase.ready }) // do one autocomplete to trigger its precompilation // TODO Do this from julia itself - this.client.send("complete", { query: "sq" }, { notebook_id: this.state.notebook.notebook_id }) + this.client.send('complete', { query: 'sq' }, { notebook_id: this.state.notebook.notebook_id }) setTimeout(init_feedback, 2 * 1000) // 2 seconds - load feedback a little later for snappier UI } @@ -666,7 +667,7 @@ patch: ${JSON.stringify( const on_connection_status = (val) => this.setState({ connected: val }) const on_reconnect = () => { - console.warn("Reconnected! Checking states") + console.warn('Reconnected! Checking states') return true } @@ -698,8 +699,8 @@ patch: ${JSON.stringify( }) this.on_disable_ui = () => { - document.body.classList.toggle("disable_ui", this.state.disable_ui) - document.head.querySelector("link[data-pluto-file='hide-ui']").setAttribute("media", this.state.disable_ui ? "all" : "print") + document.body.classList.toggle('disable_ui', this.state.disable_ui) + document.head.querySelector("link[data-pluto-file='hide-ui']").setAttribute('media', this.state.disable_ui ? 'all' : 'print') //@ts-ignore this.actions = this.state.disable_ui || (launch_params.slider_server_url != null && !this.state.connected) ? this.fake_actions : this.real_actions //heyo } @@ -729,14 +730,14 @@ patch: ${JSON.stringify( } setInterval(() => { - if (!this.state.static_preview && document.visibilityState === "visible") { + if (!this.state.static_preview && document.visibilityState === 'visible') { // view stats on https://stats.plutojl.org/ //@ts-ignore - count_stat(`editing/${window?.version_info?.pluto ?? "unknown"}`) + count_stat(`editing/${window?.version_info?.pluto ?? 'unknown'}`) } }, 1000 * 15 * 60) setInterval(() => { - if (!this.state.static_preview && document.visibilityState === "visible") { + if (!this.state.static_preview && document.visibilityState === 'visible') { update_stored_recent_notebooks(this.state.notebook.path) } }, 1000 * 5) @@ -775,14 +776,14 @@ patch: ${JSON.stringify( // this will no longer be necessary // console.log(`this.notebook_is_idle():`, this.notebook_is_idle()) if (!this.notebook_is_idle()) { - let changes_involving_bonds = changes.filter((x) => x.path[0] === "bonds") + let changes_involving_bonds = changes.filter((x) => x.path[0] === 'bonds') this.bonds_changes_to_apply_when_done = [...this.bonds_changes_to_apply_when_done, ...changes_involving_bonds] - changes = changes.filter((x) => x.path[0] !== "bonds") + changes = changes.filter((x) => x.path[0] !== 'bonds') } if (DEBUG_DIFFING) { try { - let previous_function_name = new Error().stack.split("\n")[2].trim().split(" ")[1] + let previous_function_name = new Error().stack.split('\n')[2].trim().split(' ')[1] console.log(`Changes to send to server from "${previous_function_name}":`, changes) } catch (error) {} } @@ -791,16 +792,16 @@ patch: ${JSON.stringify( } for (let change of changes) { - if (change.path.some((x) => typeof x === "number")) { - throw new Error("This sounds like it is editing an array...") + if (change.path.some((x) => typeof x === 'number')) { + throw new Error('This sounds like it is editing an array...') } } pending_local_updates++ this.setState({ update_is_ongoing: pending_local_updates > 0 }) try { await Promise.all([ - this.client.send("update_notebook", { updates: changes }, { notebook_id: this.state.notebook.notebook_id }, false).then((response) => { - if (response.message.response.update_went_well === "👎") { + this.client.send('update_notebook', { updates: changes }, { notebook_id: this.state.notebook.notebook_id }, false).then((response) => { + if (response.message.response.update_went_well === '👎') { // We only throw an error for functions that are waiting for this // Notebook state will already have the changes reversed throw new Error(`Pluto update_notebook error: ${response.message.response.why_not})`) @@ -823,7 +824,7 @@ patch: ${JSON.stringify( //@ts-ignore window.shutdownNotebook = this.close = () => { this.client.send( - "shutdown_notebook", + 'shutdown_notebook', { keep_in_session: false, }, @@ -839,8 +840,8 @@ patch: ${JSON.stringify( return } if (!this.state.notebook.in_temp_dir) { - if (!confirm("Are you sure? Will move from\n\n" + old_path + "\n\nto\n\n" + new_path)) { - throw new Error("Declined by user") + if (!confirm('Are you sure? Will move from\n\n' + old_path + '\n\nto\n\n' + new_path)) { + throw new Error('Declined by user') } } @@ -854,7 +855,7 @@ patch: ${JSON.stringify( // @ts-ignore document.activeElement?.blur() } catch (error) { - alert("Failed to move file:\n\n" + error.message) + alert('Failed to move file:\n\n' + error.message) } finally { this.setState({ moving_file: false }) } @@ -878,41 +879,41 @@ patch: ${JSON.stringify( } } - document.addEventListener("keyup", (e) => { - document.body.classList.toggle("ctrl_down", has_ctrl_or_cmd_pressed(e)) + document.addEventListener('keyup', (e) => { + document.body.classList.toggle('ctrl_down', has_ctrl_or_cmd_pressed(e)) }) - document.addEventListener("visibilitychange", (e) => { - document.body.classList.toggle("ctrl_down", false) + document.addEventListener('visibilitychange', (e) => { + document.body.classList.toggle('ctrl_down', false) setTimeout(() => { - document.body.classList.toggle("ctrl_down", false) + document.body.classList.toggle('ctrl_down', false) }, 100) }) - document.addEventListener("keydown", (e) => { - document.body.classList.toggle("ctrl_down", has_ctrl_or_cmd_pressed(e)) + document.addEventListener('keydown', (e) => { + document.body.classList.toggle('ctrl_down', has_ctrl_or_cmd_pressed(e)) // if (e.defaultPrevented) { // return // } - if (e.key.toLowerCase() === "q" && has_ctrl_or_cmd_pressed(e)) { + if (e.key.toLowerCase() === 'q' && has_ctrl_or_cmd_pressed(e)) { // This one can't be done as cmd+q on mac, because that closes chrome - Dral if (Object.values(this.state.notebook.cell_results).some((c) => c.running || c.queued)) { this.actions.interrupt_remote() } e.preventDefault() - } else if (e.key.toLowerCase() === "s" && has_ctrl_or_cmd_pressed(e)) { + } else if (e.key.toLowerCase() === 's' && has_ctrl_or_cmd_pressed(e)) { const some_cells_ran = this.actions.set_and_run_all_changed_remote_cells() if (!some_cells_ran) { // all cells were in sync allready // TODO: let user know that the notebook autosaves } e.preventDefault() - } else if (e.key === "Backspace" || e.key === "Delete") { - if (this.delete_selected("Delete")) { + } else if (e.key === 'Backspace' || e.key === 'Delete') { + if (this.delete_selected('Delete')) { e.preventDefault() } - } else if (e.key === "Enter" && e.shiftKey) { + } else if (e.key === 'Enter' && e.shiftKey) { this.run_selected() - } else if ((e.key === "?" && has_ctrl_or_cmd_pressed(e)) || e.key === "F1") { + } else if ((e.key === '?' && has_ctrl_or_cmd_pressed(e)) || e.key === 'F1') { // On mac "cmd+shift+?" is used by chrome, so that is why this needs to be ctrl as well on mac // Also pressing "ctrl+shift" on mac causes the key to show up as "/", this madness // I hope we can find a better solution for this later - Dral @@ -942,18 +943,18 @@ patch: ${JSON.stringify( if (this.state.disable_ui && this.state.offer_binder) { // const code = e.key.charCodeAt(0) - if (e.key === "Enter" || e.key.length === 1) { - if (!document.body.classList.contains("wiggle_binder")) { - document.body.classList.add("wiggle_binder") + if (e.key === 'Enter' || e.key.length === 1) { + if (!document.body.classList.contains('wiggle_binder')) { + document.body.classList.add('wiggle_binder') setTimeout(() => { - document.body.classList.remove("wiggle_binder") + document.body.classList.remove('wiggle_binder') }, 1000) } } } }) - document.addEventListener("copy", (e) => { + document.addEventListener('copy', (e) => { if (!in_textarea_or_input()) { const serialized = this.serialize_selected() if (serialized) { @@ -964,7 +965,7 @@ patch: ${JSON.stringify( } }) - document.addEventListener("cut", (e) => { + document.addEventListener('cut', (e) => { // Disabled because we don't want to accidentally delete cells // or we can enable it with a prompt // Even better would be excel style: grey out until you paste it. If you paste within the same notebook, then it is just a move. @@ -981,8 +982,8 @@ patch: ${JSON.stringify( // } }) - document.addEventListener("paste", async (e) => { - const topaste = e.clipboardData.getData("text/plain") + document.addEventListener('paste', async (e) => { + const topaste = e.clipboardData.getData('text/plain') const deserializer = detect_deserializer(topaste) if (deserializer != null) { this.actions.add_deserialized_cells(topaste, -1, deserializer) @@ -990,22 +991,22 @@ patch: ${JSON.stringify( } }) - window.addEventListener("beforeunload", (event) => { + window.addEventListener('beforeunload', (event) => { const unsaved_cells = this.state.notebook.cell_order.filter( (id) => this.state.cell_inputs_local[id] && this.state.notebook.cell_inputs[id].code !== this.state.cell_inputs_local[id].code ) const first_unsaved = unsaved_cells[0] if (first_unsaved != null) { - window.dispatchEvent(new CustomEvent("cell_focus", { detail: { cell_id: first_unsaved } })) + window.dispatchEvent(new CustomEvent('cell_focus', { detail: { cell_id: first_unsaved } })) // } else if (this.state.notebook.in_temp_dir) { // window.scrollTo(0, 0) // // TODO: focus file picker - console.log("Preventing unload") + console.log('Preventing unload') event.stopImmediatePropagation() event.preventDefault() - event.returnValue = "" + event.returnValue = '' } else { - console.warn("unloading 👉 disconnecting websocket") + console.warn('unloading 👉 disconnecting websocket') //@ts-ignore if (window.shutdown_binder != null) { // hmmmm that would also shut down the binder if you refreshed, or if you navigate to the binder session main menu by clicking the pluto logo. @@ -1027,7 +1028,7 @@ patch: ${JSON.stringify( update_stored_recent_notebooks(new_state.notebook.path, old_state?.notebook?.path) } if (old_state?.notebook?.shortpath !== new_state.notebook.shortpath) { - document.title = "🎈 " + new_state.notebook.shortpath + " — Pluto.jl" + document.title = '🎈 ' + new_state.notebook.shortpath + ' — Pluto.jl' } Object.entries(this.cached_status).forEach((e) => { @@ -1067,19 +1068,21 @@ patch: ${JSON.stringify( const status = this.cached_status ?? statusmap(this.state) const statusval = first_true_key(status) - if(launch_params.isolated_cell_ids.length > 0) { + if (launch_params.isolated_cell_ids.length > 0) { return html` <${PlutoContext.Provider} value=${this.actions}> <${PlutoBondsContext.Provider} value=${this.state.notebook.bonds}> <${PlutoJSInitializingContext.Provider} value=${this.js_init_set}>
- ${this.state.notebook.cell_order.map((cell_id, i) => html` - <${IsolatedCell} - cell_id=${cell_id} - cell_results=${this.state.notebook.cell_results[cell_id]} - hidden=${!launch_params.isolated_cell_ids.includes(cell_id)} - /> - `)} + ${this.state.notebook.cell_order.map( + (cell_id, i) => html` + <${IsolatedCell} + cell_id=${cell_id} + cell_results=${this.state.notebook.cell_results[cell_id]} + hidden=${!launch_params.isolated_cell_ids.includes(cell_id)} + /> + ` + )}
@@ -1091,7 +1094,7 @@ patch: ${JSON.stringify( href="#" onClick=${() => { this.client.send( - "restart_process", + 'restart_process', {}, { notebook_id: notebook.notebook_id, @@ -1111,10 +1114,10 @@ patch: ${JSON.stringify( <${PlutoJSInitializingContext.Provider} value=${this.js_init_set}> <${Scroller} active=${this.state.scroller} /> <${ProgressBar} notebook=${this.state.notebook} binder_phase=${this.state.binder_phase} status=${status}/> -
+
<${ExportBanner} - notebookfile_url=${export_url("notebookfile")} - notebookexport_url=${export_url("notebookexport")} + notebookfile_url=${export_url('notebookfile')} + notebookexport_url=${export_url('notebookexport')} open=${export_menu_open} onClose=${() => this.setState({ export_menu_open: false })} /> @@ -1131,24 +1134,24 @@ patch: ${JSON.stringify(

Pluto.jl

${ this.state.binder_phase === BinderPhase.ready - ? html`Save notebook...` + ? html`Save notebook...` : html`<${FilePicker} client=${this.client} - value=${notebook.in_temp_dir ? "" : notebook.path} + value=${notebook.in_temp_dir ? '' : notebook.path} on_submit=${this.submit_file_change} suggest_new_file=${{ - base: this.client.session_options == null ? "" : this.client.session_options.server.notebook_path_suggestion, + base: this.client.session_options == null ? '' : this.client.session_options.server.notebook_path_suggestion, name: notebook.shortpath, }} placeholder="Save notebook..." - button_label=${notebook.in_temp_dir ? "Choose" : "Move"} + button_label=${notebook.in_temp_dir ? 'Choose' : 'Move'} />` }
@@ -1157,19 +1160,19 @@ patch: ${JSON.stringify( }}>
${ status.binder && status.loading - ? "Loading binder..." - : statusval === "disconnected" - ? "Reconnecting..." - : statusval === "loading" - ? "Loading..." - : statusval === "nbpkg_restart_required" - ? html`${restart_button("Restart notebook")}${" (required)"}` - : statusval === "nbpkg_restart_recommended" - ? html`${restart_button("Restart notebook")}${" (recommended)"}` - : statusval === "process_restarting" - ? "Process exited — restarting..." - : statusval === "process_dead" - ? html`${"Process exited — "}${restart_button("restart")}` + ? 'Loading binder...' + : statusval === 'disconnected' + ? 'Reconnecting...' + : statusval === 'loading' + ? 'Loading...' + : statusval === 'nbpkg_restart_required' + ? html`${restart_button('Restart notebook')}${' (required)'}` + : statusval === 'nbpkg_restart_recommended' + ? html`${restart_button('Restart notebook')}${' (recommended)'}` + : statusval === 'process_restarting' + ? 'Process exited — restarting...' + : statusval === 'process_dead' + ? html`${'Process exited — '}${restart_button('restart')}` : null }
@@ -1272,13 +1275,13 @@ patch: ${JSON.stringify( // TODO This is now stored locally, lets store it somewhere central 😈 export const update_stored_recent_notebooks = (recent_path, also_delete = undefined) => { if (recent_path != null && recent_path !== default_path) { - const stored_string = localStorage.getItem("recent notebooks") + const stored_string = localStorage.getItem('recent notebooks') const stored_list = stored_string != null ? JSON.parse(stored_string) : [] const oldpaths = stored_list const newpaths = [recent_path, ...oldpaths.filter((path) => path !== recent_path && path !== also_delete)] if (!_.isEqual(oldpaths, newpaths)) { - localStorage.setItem("recent notebooks", JSON.stringify(newpaths.slice(0, 50))) + localStorage.setItem('recent notebooks', JSON.stringify(newpaths.slice(0, 50))) } } } From 376be7134afb227c1c4e9b1e6363a8122b2e7000 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Thu, 23 Sep 2021 21:29:20 +0200 Subject: [PATCH 02/18] Update editor.css --- frontend/editor.css | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/editor.css b/frontend/editor.css index 253b7426e2..bca7dff29f 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -39,6 +39,7 @@ html { body { margin: 0px; + padding: 0px; overflow-anchor: none; overflow-x: hidden; position: relative; From 932e4ca8468b9793faa9392544d20de3a654dc5f Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Wed, 20 Oct 2021 20:13:00 +0200 Subject: [PATCH 03/18] Fix https://github.com/JuliaComputing/pluto-vscode/issues/1 --- frontend/editor.css | 1 + frontend/editor.js | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/frontend/editor.css b/frontend/editor.css index bca7dff29f..1313870ace 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -67,6 +67,7 @@ body:not(.disable_ui) main { body:not(.disable_ui) { overscroll-behavior-x: contain; + background: white; } /* | main=25px+700px+6px=731px | pluto-helpbox=350px - 500px | */ diff --git a/frontend/editor.js b/frontend/editor.js index 7ee766bedd..2d34473896 100644 --- a/frontend/editor.js +++ b/frontend/editor.js @@ -2,6 +2,12 @@ import { html, render } from "./imports/Preact.js" import "./common/NodejsCompatibilityPolyfill.js" import { Editor } from "./components/Editor.js" +import { available as vscode_available } from "./common/VSCodeApi.js" + +// remove default stylesheet inserted by VS Code +if (vscode_available) { + document.head.querySelector("style#_defaultStyles").remove() +} // it's like a Rube Goldberg machine render(html`<${Editor} />`, document.body) From 5bf0145cf527d495c17b18b3ffb54691f148f1c8 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Wed, 20 Oct 2021 21:02:50 +0200 Subject: [PATCH 04/18] Fix https://github.com/JuliaComputing/pluto-vscode/issues/2 --- frontend/common/Binder.js | 1 + frontend/common/Feedback.js | 43 ++++++++++++++------------- frontend/common/PlutoConnection.js | 29 ++++++++++-------- frontend/common/alert_confirm.js | 35 ++++++++++++++++++++++ frontend/components/Editor.js | 9 ++++-- frontend/components/LiveDocs.js | 1 + frontend/components/PkgPopup.js | 5 ++-- frontend/components/Welcome.js | 7 +++-- frontend/components/useDropHandler.js | 5 ++-- 9 files changed, 91 insertions(+), 44 deletions(-) create mode 100644 frontend/common/alert_confirm.js diff --git a/frontend/common/Binder.js b/frontend/common/Binder.js index 65a5cd181b..9c5e2b9462 100644 --- a/frontend/common/Binder.js +++ b/frontend/common/Binder.js @@ -1,5 +1,6 @@ import immer from "../imports/immer.js" import { timeout_promise, ws_address_from_base } from "./PlutoConnection.js" +import { alert, confirm } from "./alert_confirm.js" export const BinderPhase = { wait_for_user: 0, diff --git a/frontend/common/Feedback.js b/frontend/common/Feedback.js index 0d074f2851..7e17b419e0 100644 --- a/frontend/common/Feedback.js +++ b/frontend/common/Feedback.js @@ -1,7 +1,8 @@ -import { timeout_promise } from "./PlutoConnection.js" +import { timeout_promise } from './PlutoConnection.js' // Sorry Fons, even this part of the code is now unnessarily overengineered. // But at least, I overengineered this on purpose. - DRAL +import { alert, confirm } from './alert_confirm.js' let async = async (async) => async() @@ -11,27 +12,27 @@ const init_firebase = async () => { firebase_load_promise = async(async () => { let [{ initializeApp }, firestore_module] = await Promise.all([ // @ts-ignore - import("https://www.gstatic.com/firebasejs/9.3.0/firebase-app.js"), + import('https://www.gstatic.com/firebasejs/9.3.0/firebase-app.js'), // @ts-ignore - import("https://www.gstatic.com/firebasejs/9.3.0/firebase-firestore.js"), + import('https://www.gstatic.com/firebasejs/9.3.0/firebase-firestore.js'), ]) let { getFirestore, addDoc, doc, collection } = firestore_module // @ts-ignore let app = initializeApp({ - apiKey: "AIzaSyC0DqEcaM8AZ6cvApXuNcNU2RgZZOj7F68", - authDomain: "localhost", - projectId: "pluto-feedback", + apiKey: 'AIzaSyC0DqEcaM8AZ6cvApXuNcNU2RgZZOj7F68', + authDomain: 'localhost', + projectId: 'pluto-feedback', }) let db = getFirestore(app) - let feedback_db = collection(db, "feedback") + let feedback_db = collection(db, 'feedback') let add_feedback = async (feedback) => { await addDoc(feedback_db, feedback) } - console.log("🔥base loaded") + console.log('🔥base loaded') // @ts-ignore return add_feedback @@ -43,9 +44,9 @@ const init_firebase = async () => { export const init_feedback = async () => { try { // Only load firebase when the feedback form is touched - const feedbackform = document.querySelector("form#feedback") - feedbackform.addEventListener("submit", (e) => { - const email = prompt("Would you like us to contact you?\n\nEmail: (leave blank to stay anonymous 👀)") + const feedbackform = document.querySelector('form#feedback') + feedbackform.addEventListener('submit', (e) => { + const email = prompt('Would you like us to contact you?\n\nEmail: (leave blank to stay anonymous 👀)') e.preventDefault() @@ -55,21 +56,21 @@ export const init_feedback = async () => { await timeout_promise( add_feedback({ // @ts-ignore - feedback: new FormData(e.target).get("opinion"), + feedback: new FormData(e.target).get('opinion'), // @ts-ignore timestamp: Date.now(), - email: email ? email : "", + email: email ? email : '', }), 5000 ) - let message = "Submitted. Thank you for your feedback! 💕" + let message = 'Submitted. Thank you for your feedback! 💕' console.log(message) alert(message) // @ts-ignore - feedbackform.querySelector("#opinion").value = "" + feedbackform.querySelector('#opinion').value = '' } catch (error) { let message = - "Whoops, failed to send feedback 😢\nWe would really like to hear from you! Please got to https://github.com/fonsp/Pluto.jl/issues to report this failure:\n\n" + 'Whoops, failed to send feedback 😢\nWe would really like to hear from you! Please got to https://github.com/fonsp/Pluto.jl/issues to report this failure:\n\n' console.error(message) console.error(error) alert(message + error) @@ -77,17 +78,17 @@ export const init_feedback = async () => { }) }) - feedbackform.addEventListener("focusin", () => { + feedbackform.addEventListener('focusin', () => { // Start loading firebase when someone interacts with the form init_firebase() }) } catch (error) { - console.error("Something went wrong loading the feedback form:", error) + console.error('Something went wrong loading the feedback form:', error) // @ts-ignore - document.querySelector("form#feedback").style.opacity = 0 - for (let char of "Oh noooooooooooooooooo...") { + document.querySelector('form#feedback').style.opacity = 0 + for (let char of 'Oh noooooooooooooooooo...') { // @ts-ignore - document.querySelector("form#feedback input").value += char + document.querySelector('form#feedback input').value += char await new Promise((resolve) => setTimeout(resolve, 200)) } } diff --git a/frontend/common/PlutoConnection.js b/frontend/common/PlutoConnection.js index 3b55e9862a..409555abdc 100644 --- a/frontend/common/PlutoConnection.js +++ b/frontend/common/PlutoConnection.js @@ -3,6 +3,7 @@ import { pack, unpack } from "./MsgPack.js" import { base64_arraybuffer, decode_base64_to_arraybuffer } from "./PlutoHash.js" import "./Polyfill.js" import { available as vscode_available, api as vscode_api } from "./VSCodeApi.js" +import { alert, confirm } from "./alert_confirm.js" // https://github.com/denysdovhan/wtfjs/issues/61 const different_Infinity_because_js_is_yuck = 2147483646 @@ -64,7 +65,7 @@ export const resolvable_promise = () => { /** * @returns {string} */ -const get_unique_short_id = () => crypto.getRandomValues(new Uint32Array(1))[0].toString(36) +export const get_unique_short_id = () => crypto.getRandomValues(new Uint32Array(1))[0].toString(36) const socket_is_alright = (socket) => socket.readyState == WebSocket.OPEN || socket.readyState == WebSocket.CONNECTING @@ -193,16 +194,18 @@ const create_vscode_connection = (address, { on_message, on_socket_close }, time try { const raw = event.data // The json-encoded data that the extension sent console.log("raw", raw) - const buffer = await decode_base64_to_arraybuffer(raw.base64_encoded) - const message = unpack(new Uint8Array(buffer)) - - try { - console.info("message received!", message) - on_message(message) - } catch (process_err) { - console.error("Failed to process message from websocket", process_err, { message }) - // prettier-ignore - alert(`Something went wrong! You might need to refresh the page.\n\nPlease open an issue on https://github.com/fonsp/Pluto.jl with this info:\n\nFailed to process update\n${process_err.message}\n\n${JSON.stringify(event)}`) + if (raw.type === "ws_proxy") { + const buffer = await decode_base64_to_arraybuffer(raw.base64_encoded) + const message = unpack(new Uint8Array(buffer)) + + try { + console.info("message received!", message) + on_message(message) + } catch (process_err) { + console.error("Failed to process message from websocket", process_err, { message }) + // prettier-ignore + alert(`Something went wrong! You might need to refresh the page.\n\nPlease open an issue on https://github.com/fonsp/Pluto.jl with this info:\n\nFailed to process update\n${process_err.message}\n\n${JSON.stringify(event)}`) + } } } catch (unpack_err) { console.error("Failed to unpack message from websocket", unpack_err, { event }) @@ -216,7 +219,7 @@ const create_vscode_connection = (address, { on_message, on_socket_close }, time const send_encoded = async (message) => { console.log("Sending message!", message) const encoded = pack(message) - await vscode_api.postMessage({ base64_encoded: await base64_arraybuffer(encoded) }) + await vscode_api.postMessage({ type: "ws_proxy", base64_encoded: await base64_arraybuffer(encoded) }) } let last_task = Promise.resolve() @@ -438,7 +441,7 @@ export const create_pluto_connection = async ({ if (connect_metadata.notebook_id != null && !u.message.notebook_exists) { // https://github.com/fonsp/Pluto.jl/issues/55 - if (confirm("A new server was started - this notebook session is no longer running.\n\nWould you like to go back to the main menu?")) { + if (await confirm("A new server was started - this notebook session is no longer running.\n\nWould you like to go back to the main menu?")) { window.location.href = "./" } on_connection_status(false) diff --git a/frontend/common/alert_confirm.js b/frontend/common/alert_confirm.js new file mode 100644 index 0000000000..f8cf5e007a --- /dev/null +++ b/frontend/common/alert_confirm.js @@ -0,0 +1,35 @@ +import { available, api } from "../common/VSCodeApi.js" +import { resolvable_promise, get_unique_short_id } from "./PlutoConnection.js" + +const sent_requests = new Map() + +if (available) { + window.addEventListener("message", (event) => { + const raw = event.data + + if (raw.type === "alert_confirm_callback") { + const request = sent_requests.get(raw.token) + if (request) { + request(raw.value) + sent_requests.delete(raw.token) + } + } + }) +} + +const create_alert_confirm = (name) => (x) => + new Promise((resolve) => { + let request_id = get_unique_short_id() + + sent_requests.set(request_id, (response_message) => { + resolve(response_message) + }) + api.postMessage({ + type: name, + text: x, + token: request_id, + }) + }) + +export const alert = available ? create_alert_confirm("alert") : window.alert +export const confirm = available ? create_alert_confirm("confirm") : window.confirm diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 1434eb7c0f..f1fa58f4c7 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -34,6 +34,8 @@ import { IsolatedCell } from './Cell.js' import { available as vscode_available } from '../common/VSCodeApi.js' const default_path = '...' +import { alert, confirm } from '../common/alert_confirm.js' + const DEBUG_DIFFING = false let pending_local_updates = 0 // from our friends at https://stackoverflow.com/a/2117523 @@ -433,9 +435,10 @@ export class Editor extends Component { return await this.actions.add_remote_cell_at(index + delta, code) }, confirm_delete_multiple: async (verb, cell_ids) => { - if (cell_ids.length <= 1 || confirm(`${verb} ${cell_ids.length} cells?`)) { + console.log(confirm) + if (cell_ids.length <= 1 || (await confirm(`${verb} ${cell_ids.length} cells?`))) { if (cell_ids.some((cell_id) => this.state.notebook.cell_results[cell_id].running || this.state.notebook.cell_results[cell_id].queued)) { - if (confirm('This cell is still running - would you like to interrupt the notebook?')) { + if (await confirm('This cell is still running - would you like to interrupt the notebook?')) { this.actions.interrupt_remote(cell_ids[0]) } } else { @@ -840,7 +843,7 @@ patch: ${JSON.stringify( return } if (!this.state.notebook.in_temp_dir) { - if (!confirm('Are you sure? Will move from\n\n' + old_path + '\n\nto\n\n' + new_path)) { + if (!(await confirm('Are you sure? Will move from\n\n' + old_path + '\n\nto\n\n' + new_path))) { throw new Error('Declined by user') } } diff --git a/frontend/components/LiveDocs.js b/frontend/components/LiveDocs.js index 3e5559b4f2..0ee3d8acf7 100644 --- a/frontend/components/LiveDocs.js +++ b/frontend/components/LiveDocs.js @@ -2,6 +2,7 @@ import { html, useState, useRef, useLayoutEffect, useEffect, useMemo, useContext import immer from "../imports/immer.js" import observablehq from "../common/SetupCellEnvironment.js" import { cl } from "../common/ClassTable.js" +import { alert, confirm } from "../common/alert_confirm.js" import { RawHTMLContainer, highlight } from "./CellOutput.js" import { PlutoContext } from "../common/PlutoContext.js" diff --git a/frontend/components/PkgPopup.js b/frontend/components/PkgPopup.js index d7a23325d5..d97c40046e 100644 --- a/frontend/components/PkgPopup.js +++ b/frontend/components/PkgPopup.js @@ -8,6 +8,7 @@ import { PlutoContext } from "../common/PlutoContext.js" import { package_status, nbpkg_fingerprint_without_terminal } from "./PkgStatusMark.js" import { PkgTerminalView } from "./PkgTerminalView.js" import { useDebouncedTruth } from "./RunArea.js" +import { alert, confirm } from "../common/alert_confirm.js" export const PkgPopup = ({ notebook }) => { let pluto_actions = useContext(PlutoContext) @@ -108,11 +109,11 @@ export const PkgPopup = ({ notebook }) => { title="Update packages" style=${(!!showupdate ? "" : "opacity: .4;") + (recent_event?.is_disable_pkg ? "display: none;" : "")} href="#" - onClick=${(e) => { + onClick=${async (e) => { if (busy) { alert("Pkg is currently busy with other packages... come back later!") } else { - if (confirm("Would you like to check for updates and install them? A backup of the notebook file will be created.")) { + if (await confirm("Would you like to check for updates and install them? A backup of the notebook file will be created.")) { console.warn("Pkg.updating!") pluto_actions.send("pkg_update", {}, { notebook_id: notebook.notebook_id }) } diff --git a/frontend/components/Welcome.js b/frontend/components/Welcome.js index cbb1fadf98..148c05404c 100644 --- a/frontend/components/Welcome.js +++ b/frontend/components/Welcome.js @@ -5,6 +5,7 @@ import { FilePicker } from "./FilePicker.js" import { create_pluto_connection, fetch_pluto_releases } from "../common/PlutoConnection.js" import { cl } from "../common/ClassTable.js" import { PasteHandler } from "./PasteHandler.js" +import { alert, confirm } from "../common/alert_confirm.js" const create_empty_notebook = (path, notebook_id = null) => { return { @@ -219,20 +220,20 @@ export class Welcome extends Component { document.body.classList.add("loading") window.location.href = link_open_path(processed.path_or_url) } else { - if (confirm("Are you sure? This will download and run the file at\n\n" + processed.path_or_url)) { + if (await confirm("Are you sure? This will download and run the file at\n\n" + processed.path_or_url)) { document.body.classList.add("loading") window.location.href = link_open_url(processed.path_or_url) } } } - this.on_session_click = (nb) => { + this.on_session_click = async (nb) => { if (nb.transitioning) { return } const running = nb.notebook_id != null if (running) { - if (confirm("Shut down notebook process?")) { + if (await confirm("Shut down notebook process?")) { set_notebook_state(nb.path, { running: false, transitioning: true, diff --git a/frontend/components/useDropHandler.js b/frontend/components/useDropHandler.js index 5dbb111068..8530a8dfe8 100644 --- a/frontend/components/useDropHandler.js +++ b/frontend/components/useDropHandler.js @@ -2,6 +2,7 @@ import _ from "../imports/lodash.js" import { PlutoContext } from "../common/PlutoContext.js" import { useState, useMemo, useContext } from "../imports/Preact.js" import { EditorView } from "../imports/CodemirrorPlutoSetup.js" +import { alert, confirm } from "../common/alert_confirm.js" const MAGIC_TIMEOUT = 500 const DEBOUNCE_MAGIC_MS = 250 @@ -39,11 +40,11 @@ export const useDropHandler = () => { set_saving_file(false) set_drag_active_fast(false) if (!success) { - alert("Pluto can't save this file 😥") + await alert("Pluto can't save this file 😥") return "# File save failed" } if (code) return code - alert("Pluto doesn't know what to do with this file 😥. Do you have a suggestion? Open an issue at https://github.com/fonsp/Pluto.jl") + await alert("Pluto doesn't know what to do with this file 😥. Do you have a suggestion? Open an issue at https://github.com/fonsp/Pluto.jl") return "" } return (ev) => { From 58fc26ecf1c66ba4e5dc079070c04e36abd74d18 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Thu, 21 Oct 2021 01:27:40 +0200 Subject: [PATCH 05/18] Add recipient check to support multiple clients on the same WS stream --- frontend/common/PlutoConnection.js | 8 ++++++-- frontend/components/Editor.js | 1 - src/webserver/Dynamic.jl | 3 +++ src/webserver/PutUpdates.jl | 14 +++++++++----- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/frontend/common/PlutoConnection.js b/frontend/common/PlutoConnection.js index 409555abdc..69658acd83 100644 --- a/frontend/common/PlutoConnection.js +++ b/frontend/common/PlutoConnection.js @@ -193,7 +193,6 @@ const create_vscode_connection = (address, { on_message, on_socket_close }, time last_task = last_task.then(async () => { try { const raw = event.data // The json-encoded data that the extension sent - console.log("raw", raw) if (raw.type === "ws_proxy") { const buffer = await decode_base64_to_arraybuffer(raw.base64_encoded) const message = unpack(new Uint8Array(buffer)) @@ -400,9 +399,14 @@ export const create_pluto_connection = async ({ try { ws_connection = await (vscode_available ? create_vscode_connection : create_ws_connection)(String(ws_address), { on_message: (update) => { - const by_me = update.initiator_id == client_id + const for_me = update.recipient_id === client_id + const by_me = update.initiator_id === client_id const request_id = update.request_id + if (!for_me) { + return + } + if (by_me && request_id) { const request = sent_requests.get(request_id) if (request) { diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index f1fa58f4c7..b22c8033ba 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -435,7 +435,6 @@ export class Editor extends Component { return await this.actions.add_remote_cell_at(index + delta, code) }, confirm_delete_multiple: async (verb, cell_ids) => { - console.log(confirm) if (cell_ids.length <= 1 || (await confirm(`${verb} ${cell_ids.length} cells?`))) { if (cell_ids.some((cell_id) => this.state.notebook.cell_results[cell_id].running || this.state.notebook.cell_results[cell_id].queued)) { if (await confirm('This cell is still running - would you like to interrupt the notebook?')) { diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index 2f639d7243..df7c1da905 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -177,6 +177,7 @@ Update the local state of all clients connected to this notebook. """ function send_notebook_changes!(🙋::ClientRequest; commentary::Any=nothing) notebook_dict = notebook_to_js(🙋.notebook) + @info "Connected clients" length(🙋.session.connected_clients) Set(c -> c.stream for c in 🙋.session.connected_clients) for (_, client) in 🙋.session.connected_clients if client.connected_notebook !== nothing && client.connected_notebook.notebook_id == 🙋.notebook.notebook_id current_dict = get(current_state_for_clients, client, :empty) @@ -186,6 +187,7 @@ function send_notebook_changes!(🙋::ClientRequest; commentary::Any=nothing) # Make sure we do send a confirmation to the client who made the request, even without changes is_response = 🙋.initiator !== nothing && client == 🙋.initiator.client + @info "Responding" is_response (🙋.initiator !== nothing) (commentary === nothing) if !isempty(patches) || is_response response = Dict( @@ -279,6 +281,7 @@ responses[:update_notebook] = function response_update_notebook(🙋::ClientRequ patches = (Base.convert(Firebasey.JSONPatch, update) for update in 🙋.body["updates"]) if length(patches) == 0 + @info "Empty patches" send_notebook_changes!(🙋) return nothing end diff --git a/src/webserver/PutUpdates.jl b/src/webserver/PutUpdates.jl index ea541777d0..dcac8e9dd5 100644 --- a/src/webserver/PutUpdates.jl +++ b/src/webserver/PutUpdates.jl @@ -1,6 +1,10 @@ -function serialize_message_to_stream(io::IO, message::UpdateMessage) - to_send = Dict(:type => message.type, :message => message.message) +function serialize_message_to_stream(io::IO, message::UpdateMessage, recipient::ClientSession) + to_send = Dict{Symbol,Any}( + :type => message.type, + :message => message.message, + :recipient_id => recipient.id, + ) if message.notebook !== nothing to_send[:notebook_id] = message.notebook.notebook_id end @@ -15,8 +19,8 @@ function serialize_message_to_stream(io::IO, message::UpdateMessage) pack(io, to_send) end -function serialize_message(message::UpdateMessage) - sprint(serialize_message_to_stream, message) +function serialize_message(message::UpdateMessage, recipient::ClientSession) + sprint(serialize_message_to_stream, message, recipient) end "Send `messages` to all clients connected to the `notebook`." @@ -76,7 +80,7 @@ function flushclient(client::ClientSession) if client.stream isa HTTP.WebSockets.WebSocket client.stream.frame_type = HTTP.WebSockets.WS_BINARY end - write(client.stream, serialize_message(next_to_send)) + write(client.stream, serialize_message(next_to_send, client)) else put!(flushtoken) return false From f15cbefda30cc267a47e9263b10af2ea715a577b Mon Sep 17 00:00:00 2001 From: Panagiotis Georgakopoulos Date: Tue, 9 Nov 2021 11:13:31 +0200 Subject: [PATCH 06/18] Fix quotes (consistently\!) --- .vscode/settings.json | 1 + frontend/common/Feedback.js | 44 ++--- frontend/components/Editor.js | 318 +++++++++++++++++----------------- 3 files changed, 182 insertions(+), 181 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index de54dee19e..81223557b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "prettier.tabWidth": 4, "prettier.semi": false, "prettier.quoteProps": "consistent", + "prettier.singleQuote": false, "julia.format.calls": false, "julia.format.comments": false, "julia.format.curly": false, diff --git a/frontend/common/Feedback.js b/frontend/common/Feedback.js index 7e17b419e0..06f6931994 100644 --- a/frontend/common/Feedback.js +++ b/frontend/common/Feedback.js @@ -1,8 +1,8 @@ -import { timeout_promise } from './PlutoConnection.js' +import { timeout_promise } from "./PlutoConnection.js" // Sorry Fons, even this part of the code is now unnessarily overengineered. // But at least, I overengineered this on purpose. - DRAL -import { alert, confirm } from './alert_confirm.js' +import { alert, confirm } from "./alert_confirm.js" let async = async (async) => async() @@ -12,27 +12,27 @@ const init_firebase = async () => { firebase_load_promise = async(async () => { let [{ initializeApp }, firestore_module] = await Promise.all([ // @ts-ignore - import('https://www.gstatic.com/firebasejs/9.3.0/firebase-app.js'), + import("https://www.gstatic.com/firebasejs/9.3.0/firebase-app.js"), // @ts-ignore - import('https://www.gstatic.com/firebasejs/9.3.0/firebase-firestore.js'), + import("https://www.gstatic.com/firebasejs/9.3.0/firebase-firestore.js"), ]) let { getFirestore, addDoc, doc, collection } = firestore_module // @ts-ignore let app = initializeApp({ - apiKey: 'AIzaSyC0DqEcaM8AZ6cvApXuNcNU2RgZZOj7F68', - authDomain: 'localhost', - projectId: 'pluto-feedback', + apiKey: "AIzaSyC0DqEcaM8AZ6cvApXuNcNU2RgZZOj7F68", + authDomain: "localhost", + projectId: "pluto-feedback", }) let db = getFirestore(app) - let feedback_db = collection(db, 'feedback') + let feedback_db = collection(db, "feedback") let add_feedback = async (feedback) => { await addDoc(feedback_db, feedback) } - console.log('🔥base loaded') + console.log("🔥base loaded") // @ts-ignore return add_feedback @@ -44,9 +44,9 @@ const init_firebase = async () => { export const init_feedback = async () => { try { // Only load firebase when the feedback form is touched - const feedbackform = document.querySelector('form#feedback') - feedbackform.addEventListener('submit', (e) => { - const email = prompt('Would you like us to contact you?\n\nEmail: (leave blank to stay anonymous 👀)') + const feedbackform = document.querySelector("form#feedback") + feedbackform.addEventListener("submit", (e) => { + const email = prompt("Would you like us to contact you?\n\nEmail: (leave blank to stay anonymous 👀)") e.preventDefault() @@ -56,21 +56,21 @@ export const init_feedback = async () => { await timeout_promise( add_feedback({ // @ts-ignore - feedback: new FormData(e.target).get('opinion'), + feedback: new FormData(e.target).get("opinion"), // @ts-ignore timestamp: Date.now(), - email: email ? email : '', + email: email ? email : "", }), 5000 ) - let message = 'Submitted. Thank you for your feedback! 💕' + let message = "Submitted. Thank you for your feedback! 💕" console.log(message) alert(message) // @ts-ignore - feedbackform.querySelector('#opinion').value = '' + feedbackform.querySelector("#opinion").value = "" } catch (error) { let message = - 'Whoops, failed to send feedback 😢\nWe would really like to hear from you! Please got to https://github.com/fonsp/Pluto.jl/issues to report this failure:\n\n' + "Whoops, failed to send feedback 😢\nWe would really like to hear from you! Please got to https://github.com/fonsp/Pluto.jl/issues to report this failure:\n\n" console.error(message) console.error(error) alert(message + error) @@ -78,17 +78,17 @@ export const init_feedback = async () => { }) }) - feedbackform.addEventListener('focusin', () => { + feedbackform.addEventListener("focusin", () => { // Start loading firebase when someone interacts with the form init_firebase() }) } catch (error) { - console.error('Something went wrong loading the feedback form:', error) + console.error("Something went wrong loading the feedback form:", error) // @ts-ignore - document.querySelector('form#feedback').style.opacity = 0 - for (let char of 'Oh noooooooooooooooooo...') { + document.querySelector("form#feedback").style.opacity = 0 + for (let char of "Oh noooooooooooooooooo...") { // @ts-ignore - document.querySelector('form#feedback input').value += char + document.querySelector("form#feedback input").value += char await new Promise((resolve) => setTimeout(resolve, 200)) } } diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index b22c8033ba..d719559f2e 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -1,40 +1,40 @@ -import { html, Component, useState, useEffect, useMemo } from '../imports/Preact.js' -import immer, { applyPatches, produceWithPatches } from '../imports/immer.js' -import _ from '../imports/lodash.js' - -import { create_pluto_connection } from '../common/PlutoConnection.js' -import { init_feedback } from '../common/Feedback.js' -import { serialize_cells, deserialize_cells, detect_deserializer } from '../common/Serialization.js' - -import { FilePicker } from './FilePicker.js' -import { Preamble } from './Preamble.js' -import { NotebookMemo as Notebook } from './Notebook.js' -import { LiveDocs } from './LiveDocs.js' -import { DropRuler } from './DropRuler.js' -import { SelectionArea } from './SelectionArea.js' -import { UndoDelete } from './UndoDelete.js' -import { SlideControls } from './SlideControls.js' -import { Scroller } from './Scroller.js' -import { ExportBanner } from './ExportBanner.js' -import { PkgPopup } from './PkgPopup.js' - -import { slice_utf8, length_utf8 } from '../common/UnicodeTools.js' -import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea_or_input } from '../common/KeyboardShortcuts.js' -import { handle_log } from '../common/Logging.js' -import { PlutoContext, PlutoBondsContext, PlutoJSInitializingContext } from '../common/PlutoContext.js' -import { unpack } from '../common/MsgPack.js' -import { useDropHandler } from './useDropHandler.js' -import { PkgTerminalView } from './PkgTerminalView.js' -import { start_binder, BinderPhase, count_stat } from '../common/Binder.js' -import { read_Uint8Array_with_progress, FetchProgress } from './FetchProgress.js' -import { BinderButton } from './BinderButton.js' -import { slider_server_actions, nothing_actions } from '../common/SliderServerClient.js' -import { ProgressBar } from './ProgressBar.js' -import { IsolatedCell } from './Cell.js' -import { available as vscode_available } from '../common/VSCodeApi.js' - -const default_path = '...' -import { alert, confirm } from '../common/alert_confirm.js' +import { html, Component, useState, useEffect, useMemo } from "../imports/Preact.js" +import immer, { applyPatches, produceWithPatches } from "../imports/immer.js" +import _ from "../imports/lodash.js" + +import { create_pluto_connection } from "../common/PlutoConnection.js" +import { init_feedback } from "../common/Feedback.js" +import { serialize_cells, deserialize_cells, detect_deserializer } from "../common/Serialization.js" + +import { FilePicker } from "./FilePicker.js" +import { Preamble } from "./Preamble.js" +import { NotebookMemo as Notebook } from "./Notebook.js" +import { LiveDocs } from "./LiveDocs.js" +import { DropRuler } from "./DropRuler.js" +import { SelectionArea } from "./SelectionArea.js" +import { UndoDelete } from "./UndoDelete.js" +import { SlideControls } from "./SlideControls.js" +import { Scroller } from "./Scroller.js" +import { ExportBanner } from "./ExportBanner.js" +import { PkgPopup } from "./PkgPopup.js" + +import { slice_utf8, length_utf8 } from "../common/UnicodeTools.js" +import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea_or_input } from "../common/KeyboardShortcuts.js" +import { handle_log } from "../common/Logging.js" +import { PlutoContext, PlutoBondsContext, PlutoJSInitializingContext } from "../common/PlutoContext.js" +import { unpack } from "../common/MsgPack.js" +import { useDropHandler } from "./useDropHandler.js" +import { PkgTerminalView } from "./PkgTerminalView.js" +import { start_binder, BinderPhase, count_stat } from "../common/Binder.js" +import { read_Uint8Array_with_progress, FetchProgress } from "./FetchProgress.js" +import { BinderButton } from "./BinderButton.js" +import { slider_server_actions, nothing_actions } from "../common/SliderServerClient.js" +import { ProgressBar } from "./ProgressBar.js" +import { IsolatedCell } from "./Cell.js" +import { available as vscode_available } from "../common/VSCodeApi.js" + +const default_path = "..." +import { alert, confirm } from "../common/alert_confirm.js" const DEBUG_DIFFING = false let pending_local_updates = 0 @@ -42,7 +42,7 @@ let pending_local_updates = 0 // i checked it and it generates Julia-legal UUIDs and that's all we need -SNOF const uuidv4 = () => //@ts-ignore - '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)) + "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)) /** * @typedef {import('../imports/immer').Patch} Patch @@ -51,25 +51,25 @@ const uuidv4 = () => const Main = ({ children }) => { const { handler } = useDropHandler() useEffect(() => { - document.body.addEventListener('drop', handler) - document.body.addEventListener('dragover', handler) - document.body.addEventListener('dragenter', handler) - document.body.addEventListener('dragleave', handler) + document.body.addEventListener("drop", handler) + document.body.addEventListener("dragover", handler) + document.body.addEventListener("dragenter", handler) + document.body.addEventListener("dragleave", handler) return () => { - document.body.removeEventListener('drop', handler) - document.body.removeEventListener('dragover', handler) - document.body.removeEventListener('dragenter', handler) - document.body.removeEventListener('dragleave', handler) + document.body.removeEventListener("drop", handler) + document.body.removeEventListener("dragover", handler) + document.body.removeEventListener("dragenter", handler) + document.body.removeEventListener("dragleave", handler) } }) return html`
${children}
` } const ProcessStatus = { - ready: 'ready', - starting: 'starting', - no_process: 'no_process', - waiting_to_restart: 'waiting_to_restart', + ready: "ready", + starting: "starting", + no_process: "no_process", + waiting_to_restart: "waiting_to_restart", } /** @@ -163,25 +163,25 @@ const first_true_key = (obj) => { * }} */ -const url_logo_big = document.head.querySelector("link[rel='pluto-logo-big']").getAttribute('href') -const url_logo_small = document.head.querySelector("link[rel='pluto-logo-small']").getAttribute('href') +const url_logo_big = document.head.querySelector("link[rel='pluto-logo-big']").getAttribute("href") +const url_logo_small = document.head.querySelector("link[rel='pluto-logo-small']").getAttribute("href") const url_params = new URLSearchParams(window.location.search) const launch_params = { //@ts-ignore - notebook_id: (vscode_available ? null : url_params.get('id')) ?? window.pluto_notebook_id, + notebook_id: (vscode_available ? null : url_params.get("id")) ?? window.pluto_notebook_id, //@ts-ignore - statefile: url_params.get('statefile') ?? window.pluto_statefile, + statefile: url_params.get("statefile") ?? window.pluto_statefile, //@ts-ignore - notebookfile: url_params.get('notebookfile') ?? window.pluto_notebookfile, + notebookfile: url_params.get("notebookfile") ?? window.pluto_notebookfile, //@ts-ignore - disable_ui: !!(url_params.get('disable_ui') ?? window.pluto_disable_ui), + disable_ui: !!(url_params.get("disable_ui") ?? window.pluto_disable_ui), //@ts-ignore - isolated_cell_ids: url_params.getAll('isolated_cell_id') ?? window.isolated_cell_id, + isolated_cell_ids: url_params.getAll("isolated_cell_id") ?? window.isolated_cell_id, //@ts-ignore - binder_url: url_params.get('binder_url') ?? window.pluto_binder_url, + binder_url: url_params.get("binder_url") ?? window.pluto_binder_url, //@ts-ignore - slider_server_url: url_params.get('slider_server_url') ?? window.pluto_slider_server_url, + slider_server_url: url_params.get("slider_server_url") ?? window.pluto_slider_server_url, } /** @@ -191,9 +191,9 @@ const launch_params = { const initial_notebook = () => ({ notebook_id: launch_params.notebook_id, path: default_path, - shortpath: '', + shortpath: "", in_temp_dir: true, - process_status: 'starting', + process_status: "starting", last_save_time: 0.0, last_hot_reload_time: 0.0, cell_inputs: {}, @@ -263,7 +263,7 @@ export class Editor extends Component { const new_i = i + delta if (new_i >= 0 && new_i < this.state.notebook.cell_order.length) { window.dispatchEvent( - new CustomEvent('cell_focus', { + new CustomEvent("cell_focus", { detail: { cell_id: this.state.notebook.cell_order[new_i], line: line, @@ -286,7 +286,7 @@ export class Editor extends Component { let index - if (typeof index_or_id === 'number') { + if (typeof index_or_id === "number") { index = index_or_id } else { /* if the input is not an integer, try interpreting it as a cell id */ @@ -326,7 +326,7 @@ export class Editor extends Component { notebook.cell_inputs[cell.cell_id] = { ...cell, // Fill the cell with empty code remotely, so it doesn't run unsafe code - code: '', + code: "", } } notebook.cell_order = [ @@ -336,9 +336,9 @@ export class Editor extends Component { ] }) }, - wrap_remote_cell: async (cell_id, block_start = 'begin', block_end = 'end') => { + wrap_remote_cell: async (cell_id, block_start = "begin", block_end = "end") => { const cell = this.state.notebook.cell_inputs[cell_id] - const new_code = `${block_start}\n\t${cell.code.replace(/\n/g, '\n\t')}\n${block_end}` + const new_code = `${block_start}\n\t${cell.code.replace(/\n/g, "\n\t")}\n${block_end}` await this.setStatePromise( immer((state) => { @@ -357,7 +357,7 @@ export class Editor extends Component { const old_code = cell.code const padded_boundaries = [0, ...boundaries] /** @type {Array} */ - const parts = boundaries.map((b, i) => slice_utf8(old_code, padded_boundaries[i], b).trim()).filter((x) => x !== '') + const parts = boundaries.map((b, i) => slice_utf8(old_code, padded_boundaries[i], b).trim()).filter((x) => x !== "") /** @type {Array} */ const cells_to_add = parts.map((code) => { return { @@ -405,7 +405,7 @@ export class Editor extends Component { // }), // } // }) - this.client.send('interrupt_all', {}, { notebook_id: this.state.notebook.notebook_id }, false) + this.client.send("interrupt_all", {}, { notebook_id: this.state.notebook.notebook_id }, false) }, move_remote_cells: (cell_ids, new_index) => { update_notebook((notebook) => { @@ -414,7 +414,7 @@ export class Editor extends Component { notebook.cell_order = [...before, ...cell_ids, ...after] }) }, - add_remote_cell_at: async (index, code = '') => { + add_remote_cell_at: async (index, code = "") => { let id = uuidv4() this.setState({ last_created_cell: id }) await update_notebook((notebook) => { @@ -426,18 +426,18 @@ export class Editor extends Component { } notebook.cell_order = [...notebook.cell_order.slice(0, index), id, ...notebook.cell_order.slice(index, Infinity)] }) - await this.client.send('run_multiple_cells', { cells: [id] }, { notebook_id: this.state.notebook.notebook_id }) + await this.client.send("run_multiple_cells", { cells: [id] }, { notebook_id: this.state.notebook.notebook_id }) return id }, add_remote_cell: async (cell_id, before_or_after, code) => { const index = this.state.notebook.cell_order.indexOf(cell_id) - const delta = before_or_after == 'before' ? 0 : 1 + const delta = before_or_after == "before" ? 0 : 1 return await this.actions.add_remote_cell_at(index + delta, code) }, confirm_delete_multiple: async (verb, cell_ids) => { if (cell_ids.length <= 1 || (await confirm(`${verb} ${cell_ids.length} cells?`))) { if (cell_ids.some((cell_id) => this.state.notebook.cell_results[cell_id].running || this.state.notebook.cell_results[cell_id].queued)) { - if (await confirm('This cell is still running - would you like to interrupt the notebook?')) { + if (await confirm("This cell is still running - would you like to interrupt the notebook?")) { this.actions.interrupt_remote(cell_ids[0]) } } else { @@ -456,7 +456,7 @@ export class Editor extends Component { } notebook.cell_order = notebook.cell_order.filter((cell_id) => !cell_ids.includes(cell_id)) }) - await this.client.send('run_multiple_cells', { cells: [] }, { notebook_id: this.state.notebook.notebook_id }) + await this.client.send("run_multiple_cells", { cells: [] }, { notebook_id: this.state.notebook.notebook_id }) } } }, @@ -500,7 +500,7 @@ export class Editor extends Component { } }) ) - await this.client.send('run_multiple_cells', { cells: cell_ids }, { notebook_id: this.state.notebook.notebook_id }) + await this.client.send("run_multiple_cells", { cells: cell_ids }, { notebook_id: this.state.notebook.notebook_id }) } }, /** @@ -521,7 +521,7 @@ export class Editor extends Component { }, reshow_cell: (cell_id, objectid, dim) => { this.client.send( - 'reshow_cell', + "reshow_cell", { objectid: objectid, dim: dim, @@ -533,7 +533,7 @@ export class Editor extends Component { }, write_file: (cell_id, { file, name, type }) => { return this.client.send( - 'write_file', + "write_file", { file, name, type, path: this.state.notebook.path }, { notebook_id: this.state.notebook.notebook_id, @@ -543,7 +543,7 @@ export class Editor extends Component { ) }, get_avaible_versions: async ({ package_name, notebook_id }) => { - const { message } = await this.client.send('nbpkg_available_versions', { package_name: package_name }, { notebook_id: notebook_id }) + const { message } = await this.client.send("nbpkg_available_versions", { package_name: package_name }, { notebook_id: notebook_id }) return message.versions }, } @@ -561,9 +561,9 @@ export class Editor extends Component { // } new_notebook = applyPatches(old_state ?? state.notebook, patches) } catch (exception) { - const failing_path = String(exception).match(".*'(.*)'.*")[1].replace(/\//gi, '.') - const path_value = _.get(this.state.notebook, failing_path, 'Not Found') - console.log(String(exception).match(".*'(.*)'.*")[1].replace(/\//gi, '.'), failing_path, typeof failing_path) + const failing_path = String(exception).match(".*'(.*)'.*")[1].replace(/\//gi, ".") + const path_value = _.get(this.state.notebook, failing_path, "Not Found") + console.log(String(exception).match(".*'(.*)'.*")[1].replace(/\//gi, "."), failing_path, typeof failing_path) // The alert below is not catastrophic: the editor will try to recover. // Deactivating to be user-friendly! // alert(`Ooopsiee.`) @@ -574,16 +574,16 @@ Please report this: https://github.com/fonsp/Pluto.jl/issues adding the info bel failing path: ${failing_path} notebook previous value: ${path_value} patch: ${JSON.stringify( - patches?.find(({ path }) => path.join('') === failing_path), + patches?.find(({ path }) => path.join("") === failing_path), null, 1 )} #######################**************************########################`, exception ) - console.log('Trying to recover: Refetching notebook...') + console.log("Trying to recover: Refetching notebook...") this.client.send( - 'reset_shared_state', + "reset_shared_state", {}, { notebook_id: this.state.notebook.notebook_id, @@ -594,7 +594,7 @@ patch: ${JSON.stringify( } if (DEBUG_DIFFING) { - console.group('Update!') + console.group("Update!") for (let patch of patches) { console.group(`Patch :${patch.op}`) console.log(patch.path) @@ -623,23 +623,23 @@ patch: ${JSON.stringify( if (this.state.notebook.notebook_id === update.notebook_id) { const message = update.message switch (update.type) { - case 'notebook_diff': + case "notebook_diff": if (message?.response?.from_reset) { - console.log('Trying to reset state after failure') + console.log("Trying to reset state after failure") try { apply_notebook_patches(message.patches, initial_notebook()) } catch (exception) { - alert('Oopsie!! please refresh your browser and everything will be alright!') + alert("Oopsie!! please refresh your browser and everything will be alright!") } } else if (message.patches.length !== 0) { apply_notebook_patches(message.patches) } break - case 'log': + case "log": handle_log(message, this.state.notebook.path) break default: - console.error('Received unknown update type!', update) + console.error("Received unknown update type!", update) // alert("Something went wrong 🙈\n Try clearing your browser cache and refreshing the page") break } @@ -655,13 +655,13 @@ patch: ${JSON.stringify( // @ts-ignore window.version_info = this.client.version_info // for debugging - await this.client.send('update_notebook', { updates: [] }, { notebook_id: this.state.notebook.notebook_id }, false) + await this.client.send("update_notebook", { updates: [] }, { notebook_id: this.state.notebook.notebook_id }, false) this.setState({ initializing: false, static_preview: false, binder_phase: this.state.binder_phase == null ? null : BinderPhase.ready }) // do one autocomplete to trigger its precompilation // TODO Do this from julia itself - this.client.send('complete', { query: 'sq' }, { notebook_id: this.state.notebook.notebook_id }) + this.client.send("complete", { query: "sq" }, { notebook_id: this.state.notebook.notebook_id }) setTimeout(init_feedback, 2 * 1000) // 2 seconds - load feedback a little later for snappier UI } @@ -669,7 +669,7 @@ patch: ${JSON.stringify( const on_connection_status = (val) => this.setState({ connected: val }) const on_reconnect = () => { - console.warn('Reconnected! Checking states') + console.warn("Reconnected! Checking states") return true } @@ -701,8 +701,8 @@ patch: ${JSON.stringify( }) this.on_disable_ui = () => { - document.body.classList.toggle('disable_ui', this.state.disable_ui) - document.head.querySelector("link[data-pluto-file='hide-ui']").setAttribute('media', this.state.disable_ui ? 'all' : 'print') + document.body.classList.toggle("disable_ui", this.state.disable_ui) + document.head.querySelector("link[data-pluto-file='hide-ui']").setAttribute("media", this.state.disable_ui ? "all" : "print") //@ts-ignore this.actions = this.state.disable_ui || (launch_params.slider_server_url != null && !this.state.connected) ? this.fake_actions : this.real_actions //heyo } @@ -732,14 +732,14 @@ patch: ${JSON.stringify( } setInterval(() => { - if (!this.state.static_preview && document.visibilityState === 'visible') { + if (!this.state.static_preview && document.visibilityState === "visible") { // view stats on https://stats.plutojl.org/ //@ts-ignore - count_stat(`editing/${window?.version_info?.pluto ?? 'unknown'}`) + count_stat(`editing/${window?.version_info?.pluto ?? "unknown"}`) } }, 1000 * 15 * 60) setInterval(() => { - if (!this.state.static_preview && document.visibilityState === 'visible') { + if (!this.state.static_preview && document.visibilityState === "visible") { update_stored_recent_notebooks(this.state.notebook.path) } }, 1000 * 5) @@ -778,14 +778,14 @@ patch: ${JSON.stringify( // this will no longer be necessary // console.log(`this.notebook_is_idle():`, this.notebook_is_idle()) if (!this.notebook_is_idle()) { - let changes_involving_bonds = changes.filter((x) => x.path[0] === 'bonds') + let changes_involving_bonds = changes.filter((x) => x.path[0] === "bonds") this.bonds_changes_to_apply_when_done = [...this.bonds_changes_to_apply_when_done, ...changes_involving_bonds] - changes = changes.filter((x) => x.path[0] !== 'bonds') + changes = changes.filter((x) => x.path[0] !== "bonds") } if (DEBUG_DIFFING) { try { - let previous_function_name = new Error().stack.split('\n')[2].trim().split(' ')[1] + let previous_function_name = new Error().stack.split("\n")[2].trim().split(" ")[1] console.log(`Changes to send to server from "${previous_function_name}":`, changes) } catch (error) {} } @@ -794,16 +794,16 @@ patch: ${JSON.stringify( } for (let change of changes) { - if (change.path.some((x) => typeof x === 'number')) { - throw new Error('This sounds like it is editing an array...') + if (change.path.some((x) => typeof x === "number")) { + throw new Error("This sounds like it is editing an array...") } } pending_local_updates++ this.setState({ update_is_ongoing: pending_local_updates > 0 }) try { await Promise.all([ - this.client.send('update_notebook', { updates: changes }, { notebook_id: this.state.notebook.notebook_id }, false).then((response) => { - if (response.message.response.update_went_well === '👎') { + this.client.send("update_notebook", { updates: changes }, { notebook_id: this.state.notebook.notebook_id }, false).then((response) => { + if (response.message.response.update_went_well === "👎") { // We only throw an error for functions that are waiting for this // Notebook state will already have the changes reversed throw new Error(`Pluto update_notebook error: ${response.message.response.why_not})`) @@ -826,7 +826,7 @@ patch: ${JSON.stringify( //@ts-ignore window.shutdownNotebook = this.close = () => { this.client.send( - 'shutdown_notebook', + "shutdown_notebook", { keep_in_session: false, }, @@ -842,8 +842,8 @@ patch: ${JSON.stringify( return } if (!this.state.notebook.in_temp_dir) { - if (!(await confirm('Are you sure? Will move from\n\n' + old_path + '\n\nto\n\n' + new_path))) { - throw new Error('Declined by user') + if (!(await confirm("Are you sure? Will move from\n\n" + old_path + "\n\nto\n\n" + new_path))) { + throw new Error("Declined by user") } } @@ -857,7 +857,7 @@ patch: ${JSON.stringify( // @ts-ignore document.activeElement?.blur() } catch (error) { - alert('Failed to move file:\n\n' + error.message) + alert("Failed to move file:\n\n" + error.message) } finally { this.setState({ moving_file: false }) } @@ -881,41 +881,41 @@ patch: ${JSON.stringify( } } - document.addEventListener('keyup', (e) => { - document.body.classList.toggle('ctrl_down', has_ctrl_or_cmd_pressed(e)) + document.addEventListener("keyup", (e) => { + document.body.classList.toggle("ctrl_down", has_ctrl_or_cmd_pressed(e)) }) - document.addEventListener('visibilitychange', (e) => { - document.body.classList.toggle('ctrl_down', false) + document.addEventListener("visibilitychange", (e) => { + document.body.classList.toggle("ctrl_down", false) setTimeout(() => { - document.body.classList.toggle('ctrl_down', false) + document.body.classList.toggle("ctrl_down", false) }, 100) }) - document.addEventListener('keydown', (e) => { - document.body.classList.toggle('ctrl_down', has_ctrl_or_cmd_pressed(e)) + document.addEventListener("keydown", (e) => { + document.body.classList.toggle("ctrl_down", has_ctrl_or_cmd_pressed(e)) // if (e.defaultPrevented) { // return // } - if (e.key.toLowerCase() === 'q' && has_ctrl_or_cmd_pressed(e)) { + if (e.key.toLowerCase() === "q" && has_ctrl_or_cmd_pressed(e)) { // This one can't be done as cmd+q on mac, because that closes chrome - Dral if (Object.values(this.state.notebook.cell_results).some((c) => c.running || c.queued)) { this.actions.interrupt_remote() } e.preventDefault() - } else if (e.key.toLowerCase() === 's' && has_ctrl_or_cmd_pressed(e)) { + } else if (e.key.toLowerCase() === "s" && has_ctrl_or_cmd_pressed(e)) { const some_cells_ran = this.actions.set_and_run_all_changed_remote_cells() if (!some_cells_ran) { // all cells were in sync allready // TODO: let user know that the notebook autosaves } e.preventDefault() - } else if (e.key === 'Backspace' || e.key === 'Delete') { - if (this.delete_selected('Delete')) { + } else if (e.key === "Backspace" || e.key === "Delete") { + if (this.delete_selected("Delete")) { e.preventDefault() } - } else if (e.key === 'Enter' && e.shiftKey) { + } else if (e.key === "Enter" && e.shiftKey) { this.run_selected() - } else if ((e.key === '?' && has_ctrl_or_cmd_pressed(e)) || e.key === 'F1') { + } else if ((e.key === "?" && has_ctrl_or_cmd_pressed(e)) || e.key === "F1") { // On mac "cmd+shift+?" is used by chrome, so that is why this needs to be ctrl as well on mac // Also pressing "ctrl+shift" on mac causes the key to show up as "/", this madness // I hope we can find a better solution for this later - Dral @@ -945,18 +945,18 @@ patch: ${JSON.stringify( if (this.state.disable_ui && this.state.offer_binder) { // const code = e.key.charCodeAt(0) - if (e.key === 'Enter' || e.key.length === 1) { - if (!document.body.classList.contains('wiggle_binder')) { - document.body.classList.add('wiggle_binder') + if (e.key === "Enter" || e.key.length === 1) { + if (!document.body.classList.contains("wiggle_binder")) { + document.body.classList.add("wiggle_binder") setTimeout(() => { - document.body.classList.remove('wiggle_binder') + document.body.classList.remove("wiggle_binder") }, 1000) } } } }) - document.addEventListener('copy', (e) => { + document.addEventListener("copy", (e) => { if (!in_textarea_or_input()) { const serialized = this.serialize_selected() if (serialized) { @@ -967,7 +967,7 @@ patch: ${JSON.stringify( } }) - document.addEventListener('cut', (e) => { + document.addEventListener("cut", (e) => { // Disabled because we don't want to accidentally delete cells // or we can enable it with a prompt // Even better would be excel style: grey out until you paste it. If you paste within the same notebook, then it is just a move. @@ -984,8 +984,8 @@ patch: ${JSON.stringify( // } }) - document.addEventListener('paste', async (e) => { - const topaste = e.clipboardData.getData('text/plain') + document.addEventListener("paste", async (e) => { + const topaste = e.clipboardData.getData("text/plain") const deserializer = detect_deserializer(topaste) if (deserializer != null) { this.actions.add_deserialized_cells(topaste, -1, deserializer) @@ -993,22 +993,22 @@ patch: ${JSON.stringify( } }) - window.addEventListener('beforeunload', (event) => { + window.addEventListener("beforeunload", (event) => { const unsaved_cells = this.state.notebook.cell_order.filter( (id) => this.state.cell_inputs_local[id] && this.state.notebook.cell_inputs[id].code !== this.state.cell_inputs_local[id].code ) const first_unsaved = unsaved_cells[0] if (first_unsaved != null) { - window.dispatchEvent(new CustomEvent('cell_focus', { detail: { cell_id: first_unsaved } })) + window.dispatchEvent(new CustomEvent("cell_focus", { detail: { cell_id: first_unsaved } })) // } else if (this.state.notebook.in_temp_dir) { // window.scrollTo(0, 0) // // TODO: focus file picker - console.log('Preventing unload') + console.log("Preventing unload") event.stopImmediatePropagation() event.preventDefault() - event.returnValue = '' + event.returnValue = "" } else { - console.warn('unloading 👉 disconnecting websocket') + console.warn("unloading 👉 disconnecting websocket") //@ts-ignore if (window.shutdown_binder != null) { // hmmmm that would also shut down the binder if you refreshed, or if you navigate to the binder session main menu by clicking the pluto logo. @@ -1030,7 +1030,7 @@ patch: ${JSON.stringify( update_stored_recent_notebooks(new_state.notebook.path, old_state?.notebook?.path) } if (old_state?.notebook?.shortpath !== new_state.notebook.shortpath) { - document.title = '🎈 ' + new_state.notebook.shortpath + ' — Pluto.jl' + document.title = "🎈 " + new_state.notebook.shortpath + " — Pluto.jl" } Object.entries(this.cached_status).forEach((e) => { @@ -1096,7 +1096,7 @@ patch: ${JSON.stringify( href="#" onClick=${() => { this.client.send( - 'restart_process', + "restart_process", {}, { notebook_id: notebook.notebook_id, @@ -1116,10 +1116,10 @@ patch: ${JSON.stringify( <${PlutoJSInitializingContext.Provider} value=${this.js_init_set}> <${Scroller} active=${this.state.scroller} /> <${ProgressBar} notebook=${this.state.notebook} binder_phase=${this.state.binder_phase} status=${status}/> -
+
<${ExportBanner} - notebookfile_url=${export_url('notebookfile')} - notebookexport_url=${export_url('notebookexport')} + notebookfile_url=${export_url("notebookfile")} + notebookexport_url=${export_url("notebookexport")} open=${export_menu_open} onClose=${() => this.setState({ export_menu_open: false })} /> @@ -1136,24 +1136,24 @@ patch: ${JSON.stringify(

Pluto.jl

${ this.state.binder_phase === BinderPhase.ready - ? html`Save notebook...` + ? html`Save notebook...` : html`<${FilePicker} client=${this.client} - value=${notebook.in_temp_dir ? '' : notebook.path} + value=${notebook.in_temp_dir ? "" : notebook.path} on_submit=${this.submit_file_change} suggest_new_file=${{ - base: this.client.session_options == null ? '' : this.client.session_options.server.notebook_path_suggestion, + base: this.client.session_options == null ? "" : this.client.session_options.server.notebook_path_suggestion, name: notebook.shortpath, }} placeholder="Save notebook..." - button_label=${notebook.in_temp_dir ? 'Choose' : 'Move'} + button_label=${notebook.in_temp_dir ? "Choose" : "Move"} />` }
@@ -1162,19 +1162,19 @@ patch: ${JSON.stringify( }}>
${ status.binder && status.loading - ? 'Loading binder...' - : statusval === 'disconnected' - ? 'Reconnecting...' - : statusval === 'loading' - ? 'Loading...' - : statusval === 'nbpkg_restart_required' - ? html`${restart_button('Restart notebook')}${' (required)'}` - : statusval === 'nbpkg_restart_recommended' - ? html`${restart_button('Restart notebook')}${' (recommended)'}` - : statusval === 'process_restarting' - ? 'Process exited — restarting...' - : statusval === 'process_dead' - ? html`${'Process exited — '}${restart_button('restart')}` + ? "Loading binder..." + : statusval === "disconnected" + ? "Reconnecting..." + : statusval === "loading" + ? "Loading..." + : statusval === "nbpkg_restart_required" + ? html`${restart_button("Restart notebook")}${" (required)"}` + : statusval === "nbpkg_restart_recommended" + ? html`${restart_button("Restart notebook")}${" (recommended)"}` + : statusval === "process_restarting" + ? "Process exited — restarting..." + : statusval === "process_dead" + ? html`${"Process exited — "}${restart_button("restart")}` : null }
@@ -1277,13 +1277,13 @@ patch: ${JSON.stringify( // TODO This is now stored locally, lets store it somewhere central 😈 export const update_stored_recent_notebooks = (recent_path, also_delete = undefined) => { if (recent_path != null && recent_path !== default_path) { - const stored_string = localStorage.getItem('recent notebooks') + const stored_string = localStorage.getItem("recent notebooks") const stored_list = stored_string != null ? JSON.parse(stored_string) : [] const oldpaths = stored_list const newpaths = [recent_path, ...oldpaths.filter((path) => path !== recent_path && path !== also_delete)] if (!_.isEqual(oldpaths, newpaths)) { - localStorage.setItem('recent notebooks', JSON.stringify(newpaths.slice(0, 50))) + localStorage.setItem("recent notebooks", JSON.stringify(newpaths.slice(0, 50))) } } } From 74af7c5d97ebac3d0960c0c2a8700a182c151f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=A0=CE=B1=CE=BD=CE=B1=CE=B3=CE=B9=CF=8E=CF=84=CE=B7?= =?UTF-8?q?=CF=82=20=CE=93=CE=B5=CF=89=CF=81=CE=B3=CE=B1=CE=BA=CF=8C=CF=80?= =?UTF-8?q?=CE=BF=CF=85=CE=BB=CE=BF=CF=82?= Date: Tue, 9 Nov 2021 11:37:01 +0200 Subject: [PATCH 07/18] VSCode extension: store unsubmitted cell state in vscode (#1649) --- frontend/common/VSCodeApi.js | 32 ++++++++++++++++++++++++++------ frontend/components/Editor.js | 5 +++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/frontend/common/VSCodeApi.js b/frontend/common/VSCodeApi.js index 53bca8589e..36ffb12465 100644 --- a/frontend/common/VSCodeApi.js +++ b/frontend/common/VSCodeApi.js @@ -2,9 +2,29 @@ // @ts-ignore export const available = window.acquireVsCodeApi != null -export const api = available - ? // @ts-ignore - window.acquireVsCodeApi() - : { - postMessage: console.error, - } +// @ts-ignore +const vscode = window.acquireVsCodeApi?.() ?? {} +const store_cell_input_in_vscode_state = (cell_id, new_val) => { + if (available) { + const currentVSCodeState = vscode.getState() ?? {} + const { cell_inputs_local, ...rest } = currentVSCodeState ?? {} + console.log("This is happening", currentVSCodeState) + api.setState({ cell_inputs_local: { ...(cell_inputs_local || {}), [cell_id]: { code: new_val } }, ...(rest ?? {}) }) + } +} + +const load_cell_inputs_from_vscode_state = () => { + if (available) { + const state = vscode.getState() ?? {} + console.log("This is also happening", state) + return state.cell_inputs_local ?? {} + } + return {} +} + +export const api = { + postMessage: console.error, + ...vscode, + store_cell_input_in_vscode_state, + load_cell_inputs_from_vscode_state, +} diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index d719559f2e..45d23d6107 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -31,7 +31,7 @@ import { BinderButton } from "./BinderButton.js" import { slider_server_actions, nothing_actions } from "../common/SliderServerClient.js" import { ProgressBar } from "./ProgressBar.js" import { IsolatedCell } from "./Cell.js" -import { available as vscode_available } from "../common/VSCodeApi.js" +import { available as vscode_available, api as vscode } from "../common/VSCodeApi.js" const default_path = "..." import { alert, confirm } from "../common/alert_confirm.js" @@ -211,7 +211,7 @@ export class Editor extends Component { this.state = { notebook: /** @type {NotebookData} */ initial_notebook(), - cell_inputs_local: /** @type {{ [id: string]: CellInputData }} */ ({}), + cell_inputs_local: /** @type {{ [id: string]: CellInputData }} */ vscode.load_cell_inputs_from_vscode_state(), desired_doc_query: null, recently_deleted: /** @type {Array<{ index: number, cell: CellInputData }>} */ (null), last_update_time: 0, @@ -249,6 +249,7 @@ export class Editor extends Component { update_notebook: (...args) => this.update_notebook(...args), set_doc_query: (query) => this.setState({ desired_doc_query: query }), set_local_cell: (cell_id, new_val) => { + vscode.store_cell_input_in_vscode_state(cell_id, new_val) return this.setStatePromise( immer((state) => { state.cell_inputs_local[cell_id] = { From d4fa08468d614d45e5da6e34a69ba5ac4500568c Mon Sep 17 00:00:00 2001 From: Panagiotis Georgakopoulos Date: Mon, 15 Nov 2021 11:22:24 +0200 Subject: [PATCH 08/18] pluto-vscode issue 3: hide file manager on vscode --- frontend/common/PlutoConnection.js | 4 ++-- frontend/common/VSCodeApi.js | 2 -- frontend/components/FilePicker.js | 4 ++-- src/webserver/Dynamic.jl | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/common/PlutoConnection.js b/frontend/common/PlutoConnection.js index 69658acd83..a9aead455e 100644 --- a/frontend/common/PlutoConnection.js +++ b/frontend/common/PlutoConnection.js @@ -198,7 +198,7 @@ const create_vscode_connection = (address, { on_message, on_socket_close }, time const message = unpack(new Uint8Array(buffer)) try { - console.info("message received!", message) + window.DEBUG && console.info("message received!", message) on_message(message) } catch (process_err) { console.error("Failed to process message from websocket", process_err, { message }) @@ -216,7 +216,7 @@ const create_vscode_connection = (address, { on_message, on_socket_close }, time }) const send_encoded = async (message) => { - console.log("Sending message!", message) + window.DEBUG && console.log("Sending message!", message) const encoded = pack(message) await vscode_api.postMessage({ type: "ws_proxy", base64_encoded: await base64_arraybuffer(encoded) }) } diff --git a/frontend/common/VSCodeApi.js b/frontend/common/VSCodeApi.js index 36ffb12465..64532936fa 100644 --- a/frontend/common/VSCodeApi.js +++ b/frontend/common/VSCodeApi.js @@ -8,7 +8,6 @@ const store_cell_input_in_vscode_state = (cell_id, new_val) => { if (available) { const currentVSCodeState = vscode.getState() ?? {} const { cell_inputs_local, ...rest } = currentVSCodeState ?? {} - console.log("This is happening", currentVSCodeState) api.setState({ cell_inputs_local: { ...(cell_inputs_local || {}), [cell_id]: { code: new_val } }, ...(rest ?? {}) }) } } @@ -16,7 +15,6 @@ const store_cell_input_in_vscode_state = (cell_id, new_val) => { const load_cell_inputs_from_vscode_state = () => { if (available) { const state = vscode.getState() ?? {} - console.log("This is also happening", state) return state.cell_inputs_local ?? {} } return {} diff --git a/frontend/components/FilePicker.js b/frontend/components/FilePicker.js index 2ef2abfdb0..6336acd186 100644 --- a/frontend/components/FilePicker.js +++ b/frontend/components/FilePicker.js @@ -2,7 +2,7 @@ import { html, Component } from "../imports/Preact.js" import { utf8index_to_ut16index } from "../common/UnicodeTools.js" import { map_cmd_to_ctrl_on_mac } from "../common/KeyboardShortcuts.js" - +import { available } from "../common/VSCodeApi.js" import { EditorState, EditorSelection, @@ -187,7 +187,7 @@ export class FilePicker extends Component { // }) } render() { - return html` + return available ? null : html` diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index df7c1da905..9925092eab 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -177,7 +177,7 @@ Update the local state of all clients connected to this notebook. """ function send_notebook_changes!(🙋::ClientRequest; commentary::Any=nothing) notebook_dict = notebook_to_js(🙋.notebook) - @info "Connected clients" length(🙋.session.connected_clients) Set(c -> c.stream for c in 🙋.session.connected_clients) + # @info "Connected clients" length(🙋.session.connected_clients) Set(c -> c.stream for c in 🙋.session.connected_clients) for (_, client) in 🙋.session.connected_clients if client.connected_notebook !== nothing && client.connected_notebook.notebook_id == 🙋.notebook.notebook_id current_dict = get(current_state_for_clients, client, :empty) @@ -187,7 +187,7 @@ function send_notebook_changes!(🙋::ClientRequest; commentary::Any=nothing) # Make sure we do send a confirmation to the client who made the request, even without changes is_response = 🙋.initiator !== nothing && client == 🙋.initiator.client - @info "Responding" is_response (🙋.initiator !== nothing) (commentary === nothing) + # @info "Responding" is_response (🙋.initiator !== nothing) (commentary === nothing) if !isempty(patches) || is_response response = Dict( From 51f156c5a426bcba58699047a98e715d3448c643 Mon Sep 17 00:00:00 2001 From: Panagiotis Georgakopoulos Date: Tue, 16 Nov 2021 22:16:53 +0200 Subject: [PATCH 09/18] Override Control + S as Shift + Enter in VSCode --- frontend/components/Editor.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index eb7a8913df..631b3e73d1 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -906,6 +906,9 @@ patch: ${JSON.stringify( } e.preventDefault() } else if (e.key.toLowerCase() === "s" && has_ctrl_or_cmd_pressed(e)) { + // If VSCode is around, we shouldn't 'Set and run all remote changed remote cells. + // Control + Save sends VSCode save + if (vscode_available) return this.run_selected() const some_cells_ran = this.actions.set_and_run_all_changed_remote_cells() if (!some_cells_ran) { // all cells were in sync allready From 0afcc2b89c1070b52850d233d937a7bce25e2ff3 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Wed, 17 Nov 2021 12:12:08 +0100 Subject: [PATCH 10/18] Fix tests maybe --- src/webserver/Dynamic.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index 9925092eab..bc60c9e162 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -281,7 +281,7 @@ responses[:update_notebook] = function response_update_notebook(🙋::ClientRequ patches = (Base.convert(Firebasey.JSONPatch, update) for update in 🙋.body["updates"]) if length(patches) == 0 - @info "Empty patches" + # @info "Empty patches" send_notebook_changes!(🙋) return nothing end From 2a4e1fe3672a75d0a048ab0619dd3c6d40cd4cca Mon Sep 17 00:00:00 2001 From: Panagiotis Georgakopoulos Date: Tue, 30 Nov 2021 20:36:55 +0200 Subject: [PATCH 11/18] Add save (and more\!) listeners (experiment\!) --- src/notebook/Notebook.jl | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/notebook/Notebook.jl b/src/notebook/Notebook.jl index bcbc3964ee..65608ff07d 100644 --- a/src/notebook/Notebook.jl +++ b/src/notebook/Notebook.jl @@ -54,10 +54,26 @@ Base.@kwdef mutable struct Notebook wants_to_interrupt::Bool=false last_save_time::typeof(time())=time() last_hot_reload_time::typeof(time())=zero(time()) + last_serialized_version::String = "" + write_out_fs::Bool=true + # Listeners should get PlutoEvent as first argument + # Hook up event listeners to execute custom functionality when + # a Pluto event happens + # Current Pluto Events: + # - FileSave (Doing) + # - DirtyFileUpdate (TODO) + listeners::Vector{Function} = [] bonds::Dict{Symbol,BondValue}=Dict{Symbol,BondValue}() end +abstract type PlutoEvent end + +struct FileSaveEvent <: PlutoEvent + fileContent::String + path::String +end + Notebook(cells::Array{Cell,1}, path::AbstractString, notebook_id::UUID) = Notebook( cells_dict=Dict(map(cells) do cell (cell.cell_id, cell) @@ -175,9 +191,23 @@ end function save_notebook(notebook::Notebook, path::String) # @warn "Saving to file!!" exception=(ErrorException(""), backtrace()) notebook.last_save_time = time() - write_buffered(path) do io + new_file_content = sprint() do io save_notebook(io, notebook) end + differs = new_file_content != notebook.last_serialized_version + if differs + event = FileSaveEvent(new_file_content, notebook.path) + foreach(notebook.listeners) do f + try + f(event) + @info "Successfully run listener!" + catch + @warn "Listener failed: " f + end + end + notebook.write_out_fs && write(path, new_file_content) + notebook.last_serialized_version = new_file_content + end end save_notebook(notebook::Notebook) = save_notebook(notebook, notebook.path) From 091769c3df1d1c29bb1d640b62bb18cfc183a78d Mon Sep 17 00:00:00 2001 From: Panagiotis Georgakopoulos Date: Tue, 30 Nov 2021 23:22:49 +0200 Subject: [PATCH 12/18] Remove unnecessary stderr --- src/notebook/Notebook.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/notebook/Notebook.jl b/src/notebook/Notebook.jl index 65608ff07d..8b00bb1dd0 100644 --- a/src/notebook/Notebook.jl +++ b/src/notebook/Notebook.jl @@ -200,9 +200,8 @@ function save_notebook(notebook::Notebook, path::String) foreach(notebook.listeners) do f try f(event) - @info "Successfully run listener!" catch - @warn "Listener failed: " f + @warn "FileSaveEvent Listener failed: " f end end notebook.write_out_fs && write(path, new_file_content) From 376b08851906afc9e52e2c09e823de8d7e656675 Mon Sep 17 00:00:00 2001 From: Panagiotis Georgakopoulos Date: Wed, 1 Dec 2021 00:05:11 +0200 Subject: [PATCH 13/18] Revert Control+S override --- frontend/components/Editor.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index a192c8c2a0..e8fd14273f 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -907,9 +907,6 @@ patch: ${JSON.stringify( } e.preventDefault() } else if (e.key.toLowerCase() === "s" && has_ctrl_or_cmd_pressed(e)) { - // If VSCode is around, we shouldn't 'Set and run all remote changed remote cells. - // Control + Save sends VSCode save - if (vscode_available) return this.run_selected() const some_cells_ran = this.actions.set_and_run_all_changed_remote_cells() if (!some_cells_ran) { // all cells were in sync allready From 7ba8575dc7b3da779e94d1061085677ac4531aad Mon Sep 17 00:00:00 2001 From: Panagiotis Georgakopoulos Date: Thu, 2 Dec 2021 18:16:43 +0200 Subject: [PATCH 14/18] Add event listener provision --- src/Pluto.jl | 1 + src/notebook/Notebook.jl | 31 +------------------------------ src/webserver/Session.jl | 6 ++++++ 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/src/Pluto.jl b/src/Pluto.jl index 6a22cee633..a62dd86d6a 100644 --- a/src/Pluto.jl +++ b/src/Pluto.jl @@ -34,6 +34,7 @@ include("./analysis/Topology.jl") include("./analysis/Errors.jl") include("./analysis/TopologicalOrder.jl") include("./notebook/Notebook.jl") +include("./notebook/Events.jl") include("./webserver/Session.jl") include("./webserver/PutUpdates.jl") diff --git a/src/notebook/Notebook.jl b/src/notebook/Notebook.jl index 8b00bb1dd0..bcbc3964ee 100644 --- a/src/notebook/Notebook.jl +++ b/src/notebook/Notebook.jl @@ -54,26 +54,10 @@ Base.@kwdef mutable struct Notebook wants_to_interrupt::Bool=false last_save_time::typeof(time())=time() last_hot_reload_time::typeof(time())=zero(time()) - last_serialized_version::String = "" - write_out_fs::Bool=true - # Listeners should get PlutoEvent as first argument - # Hook up event listeners to execute custom functionality when - # a Pluto event happens - # Current Pluto Events: - # - FileSave (Doing) - # - DirtyFileUpdate (TODO) - listeners::Vector{Function} = [] bonds::Dict{Symbol,BondValue}=Dict{Symbol,BondValue}() end -abstract type PlutoEvent end - -struct FileSaveEvent <: PlutoEvent - fileContent::String - path::String -end - Notebook(cells::Array{Cell,1}, path::AbstractString, notebook_id::UUID) = Notebook( cells_dict=Dict(map(cells) do cell (cell.cell_id, cell) @@ -191,22 +175,9 @@ end function save_notebook(notebook::Notebook, path::String) # @warn "Saving to file!!" exception=(ErrorException(""), backtrace()) notebook.last_save_time = time() - new_file_content = sprint() do io + write_buffered(path) do io save_notebook(io, notebook) end - differs = new_file_content != notebook.last_serialized_version - if differs - event = FileSaveEvent(new_file_content, notebook.path) - foreach(notebook.listeners) do f - try - f(event) - catch - @warn "FileSaveEvent Listener failed: " f - end - end - notebook.write_out_fs && write(path, new_file_content) - notebook.last_serialized_version = new_file_content - end end save_notebook(notebook::Notebook) = save_notebook(notebook, notebook.path) diff --git a/src/webserver/Session.jl b/src/webserver/Session.jl index aa408dbe07..8c54e029a9 100644 --- a/src/webserver/Session.jl +++ b/src/webserver/Session.jl @@ -47,11 +47,17 @@ Base.@kwdef mutable struct ServerSession secret::String = String(rand(('a':'z') ∪ ('A':'Z') ∪ ('0':'9'), 8)) binder_token::Union{String,Nothing} = nothing options::Configuration.Options = Configuration.Options() + event_listener::Function = function(a::PlutoEvent) end end function save_notebook(session::ServerSession, notebook::Notebook) # FUTURE: fire on_save event here + try + session.event_listener(FileSaveEvent(notebook)) + catch + @warn "Couldn't run event listener" + end if !session.options.server.disable_writing_notebook_files save_notebook(notebook, notebook.path) From 88fbce2c80c26942c1756c95628cc01fa49bc6f7 Mon Sep 17 00:00:00 2001 From: Panagiotis Georgakopoulos Date: Thu, 2 Dec 2021 18:20:33 +0200 Subject: [PATCH 15/18] OOps --- src/notebook/Events.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/notebook/Events.jl diff --git a/src/notebook/Events.jl b/src/notebook/Events.jl new file mode 100644 index 0000000000..41057afb6b --- /dev/null +++ b/src/notebook/Events.jl @@ -0,0 +1,20 @@ +abstract type PlutoEvent end + +struct FileSaveEvent <: PlutoEvent + notebook::Notebook + fileContent::String + path::String +end + +FileSaveEvent(notebook::Notebook) = begin + fileContent = sprint() do io + save_notebook(io, notebook) + end + FileSaveEvent(notebook, fileContent, notebook.path) +end + +struct FileEditEvent <: PlutoEvent + notebook::Notebook + fileContent::String + path::String +end \ No newline at end of file From 12fc15932cbb6ac3bc83e4d758b590f168d069c9 Mon Sep 17 00:00:00 2001 From: Panagiotis Georgakopoulos Date: Thu, 2 Dec 2021 18:37:58 +0200 Subject: [PATCH 16/18] Use event listeners --- src/webserver/Session.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webserver/Session.jl b/src/webserver/Session.jl index 8c54e029a9..db37a81dc0 100644 --- a/src/webserver/Session.jl +++ b/src/webserver/Session.jl @@ -52,11 +52,11 @@ end function save_notebook(session::ServerSession, notebook::Notebook) - # FUTURE: fire on_save event here + # Notify event_listener from here try session.event_listener(FileSaveEvent(notebook)) - catch - @warn "Couldn't run event listener" + catch e + @warn "Couldn't run event listener" e end if !session.options.server.disable_writing_notebook_files From 2d337886154e097340e0bcffab02e1d447318cfc Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 6 Dec 2021 22:12:17 +0100 Subject: [PATCH 17/18] whitespace --- frontend/components/Editor.js | 2 ++ frontend/components/FilePicker.js | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 32b010b503..ce64bd7954 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -1160,6 +1160,8 @@ patch: ${JSON.stringify( ${ status.binder ? html`Save notebook...` + : vscode_available + ? null : html`<${FilePicker} client=${this.client} value=${notebook.in_temp_dir ? "" : notebook.path} diff --git a/frontend/components/FilePicker.js b/frontend/components/FilePicker.js index 6336acd186..067a09d2b1 100644 --- a/frontend/components/FilePicker.js +++ b/frontend/components/FilePicker.js @@ -2,7 +2,6 @@ import { html, Component } from "../imports/Preact.js" import { utf8index_to_ut16index } from "../common/UnicodeTools.js" import { map_cmd_to_ctrl_on_mac } from "../common/KeyboardShortcuts.js" -import { available } from "../common/VSCodeApi.js" import { EditorState, EditorSelection, @@ -187,7 +186,7 @@ export class FilePicker extends Component { // }) } render() { - return available ? null : html` + return html` From 8ab4a3d9b17997a96148ab1f7fb821cf2b3b6a11 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 6 Dec 2021 22:12:35 +0100 Subject: [PATCH 18/18] Restore FilePicker.js --- frontend/components/FilePicker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/components/FilePicker.js b/frontend/components/FilePicker.js index 067a09d2b1..2ef2abfdb0 100644 --- a/frontend/components/FilePicker.js +++ b/frontend/components/FilePicker.js @@ -2,6 +2,7 @@ import { html, Component } from "../imports/Preact.js" import { utf8index_to_ut16index } from "../common/UnicodeTools.js" import { map_cmd_to_ctrl_on_mac } from "../common/KeyboardShortcuts.js" + import { EditorState, EditorSelection,