Skip to content

Commit

Permalink
✂ Offer to split multiline cells (#335)
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsp authored Sep 2, 2020
1 parent 289b960 commit acc2117
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 101 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name = "Pluto"
uuid = "c3e4b0f8-55cb-11ea-2926-15256bba5781"
license = "MIT"
authors = ["Fons van der Plas <[email protected]>", "Mikołaj Bochenski <[email protected]>"]
version = "0.11.10"
version = "0.11.11"

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Expand Down
2 changes: 1 addition & 1 deletion frontend/common/OfflineHTMLExport.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const offline_html = ({ pluto_version, body, head }) => {
<link rel="stylesheet" href="${CDNified(pluto_version, "editor.css")}" type="text/css" />
<link rel="stylesheet" href="${CDNified(pluto_version, "treeview.css")}" type="text/css" />
<link rel="stylesheet" href="${CDNified(pluto_version, "hide-ui.css")}" type="text/css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.56.0/lib/codemirror.min.css" type="text/css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.57.0/lib/codemirror.min.css" type="text/css" />
<script src="${CDNified(pluto_version, "treeview.js")}"></script>
Expand Down
17 changes: 15 additions & 2 deletions frontend/common/UnicodeTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const length_utf8 = (str, startindex_utf16 = 0, endindex_utf16 = undefine

export const utf8index_to_ut16index = (str, index_utf8) => td.decode(te.encode(str).slice(0, index_utf8)).length

export const spliceUtf8 = (original, startindex_utf8, endindex_utf8, replacement) => {
export const splice_utf8 = (original, startindex_utf8, endindex_utf8, replacement) => {
// JS uses UTF-16 for internal representation of strings, e.g.
// "e".length == 1, "é".length == 1, "🐶".length == 2

Expand All @@ -28,4 +28,17 @@ export const spliceUtf8 = (original, startindex_utf8, endindex_utf8, replacement
return td.decode(result_enc)
}

console.assert(spliceUtf8("e é 🐶 is a dog", 5, 9, "hannes ❤") == "e é hannes ❤ is a dog")
export const slice_utf8 = (original, startindex_utf8, endindex_utf8) => {
// JS uses UTF-16 for internal representation of strings, e.g.
// "e".length == 1, "é".length == 1, "🐶".length == 2

// Julia uses UTF-8, e.g.
// ncodeunits("e") == 1, ncodeunits("é") == 2, ncodeunits("🐶") == 4
// length("e") == 1, length("é") == 1, length("🐶") == 1

const original_enc = te.encode(original)
return td.decode(original_enc.slice(startindex_utf8, endindex_utf8))
}

console.assert(splice_utf8("e é 🐶 is a dog", 5, 9, "hannes ❤") === "e é hannes ❤ is a dog")
console.assert(slice_utf8("e é 🐶 is a dog", 5, 9) === "🐶")
7 changes: 6 additions & 1 deletion frontend/components/Cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ export const Cell = ({
const focusListener = (e) => {
if (e.detail.cell_id === cell_id) {
if (e.detail.line != null) {
set_cm_forced_focus([{ line: e.detail.line, ch: 0 }, { line: e.detail.line, ch: Infinity }, { scroll: true }])
const ch = e.detail.ch
if (ch == null) {
set_cm_forced_focus([{ line: e.detail.line, ch: 0 }, { line: e.detail.line, ch: Infinity }, { scroll: true }])
} else {
set_cm_forced_focus([{ line: e.detail.line, ch: ch }, { line: e.detail.line, ch: ch }, { scroll: true }])
}
}
}
}
Expand Down
4 changes: 0 additions & 4 deletions frontend/components/CellInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,6 @@ export const CellInput = ({
change_handler_ref.current(new_value)
})

// cm.on("focus", () => {
// window.dispatchEvent(new CustomEvent("collapse_cell_selection", {}))
// })

cm.on("blur", () => {
// NOT a debounce:
setTimeout(() => {
Expand Down
3 changes: 2 additions & 1 deletion frontend/components/CellOutput.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class CellOutput extends Component {
inline_output:
!this.props.errored && !!this.props.body && (this.props.mime == "application/vnd.pluto.tree+xml" || this.props.mime == "text/plain"),
})}
mime=${this.props.mime}
>
<assignee>${this.props.rootassignee}</assignee>
<${OutputBody} ...${this.props} />
Expand Down Expand Up @@ -62,7 +63,7 @@ const OutputBody = ({ mime, body, cell_id, all_completed_promise, requests }) =>
case "image/bmp":
case "image/svg+xml":
const src = URL.createObjectURL(new Blob([body], { type: mime }))
return html`<div><img src=${src} /></div>`
return html`<div><img type=${mime} src=${src} /></div>`
break
case "text/html":
case "application/vnd.pluto.tree+xml":
Expand Down
168 changes: 103 additions & 65 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { link_open_path } from "./Welcome.js"
import { empty_cell_data, code_differs } from "./Cell.js"

import { offline_html } from "../common/OfflineHTMLExport.js"
import { slice_utf8, length_utf8 } from "../common/UnicodeTools.js"

const default_path = "..."

Expand All @@ -35,30 +36,34 @@ export class Editor extends Component {
loading: true,
}
// convenience method
const set_notebook_state = (updater, callback) => {
this.setState((prevstate) => {
return {
notebook: {
...prevstate.notebook,
...updater(prevstate.notebook),
},
}
}, callback)
const set_notebook_state = (updater) => {
return new Promise((resolve) => {
this.setState((prevstate) => {
return {
notebook: {
...prevstate.notebook,
...updater(prevstate.notebook),
},
}
}, resolve)
})
}
this.set_notebook_state = set_notebook_state.bind(this)

// convenience method
const set_cell_state = (cell_id, new_state_props, callback) => {
this.setState((prevstate) => {
return {
notebook: {
...prevstate.notebook,
cells: prevstate.notebook.cells.map((c) => {
return c.cell_id == cell_id ? { ...c, ...new_state_props } : c
}),
},
}
}, callback)
const set_cell_state = (cell_id, new_state_props) => {
return new Promise((resolve) => {
this.setState((prevstate) => {
return {
notebook: {
...prevstate.notebook,
cells: prevstate.notebook.cells.map((c) => {
return c.cell_id == cell_id ? { ...c, ...new_state_props } : c
}),
},
}
}, resolve)
})
}
this.set_cell_state = set_cell_state.bind(this)

Expand All @@ -71,8 +76,8 @@ export class Editor extends Component {

// these are things that can be done to the local notebook
this.actions = {
add_local_cell: (cell, new_index, callback = undefined) => {
set_notebook_state((prevstate) => {
add_local_cell: (cell, new_index) => {
return set_notebook_state((prevstate) => {
if (prevstate.cells.some((c) => c.cell_id == cell.cell_id)) {
console.warn("Tried to add cell with existing cell_id. Canceled.")
console.log(cell)
Expand All @@ -84,45 +89,40 @@ export class Editor extends Component {
return {
cells: [...before.slice(0, new_index), cell, ...before.slice(new_index)],
}
}, callback)
})
},
update_local_cell_output: (cell, { output, running, runtime, errored }, callback = undefined) => {
update_local_cell_output: (cell, { output, running, runtime, errored }) => {
this.counter_statistics.numRuns++
set_cell_state(
cell.cell_id,
{
running: running,
runtime: runtime,
errored: errored,
output: { ...output, timestamp: Date.now() },
},
callback
)
return set_cell_state(cell.cell_id, {
running: running,
runtime: runtime,
errored: errored,
output: { ...output, timestamp: Date.now() },
})
},
update_local_cell_input: (cell, by_me, code, folded, callback = undefined) => {
set_cell_state(
cell.cell_id,
{
remote_code: {
body: code,
submitted_by_me: by_me,
timestamp: Date.now(),
},
code_folded: folded,
update_local_cell_input: (cell, by_me, code, folded) => {
return set_cell_state(cell.cell_id, {
remote_code: {
body: code,
submitted_by_me: by_me,
timestamp: Date.now(),
},
callback
)
local_code: {
body: code,
},
code_folded: folded,
})
},
delete_local_cell: (cell, callback = undefined) => {
delete_local_cell: (cell) => {
// TODO: event listeners? gc?
set_notebook_state((prevstate) => {
return set_notebook_state((prevstate) => {
return {
cells: prevstate.cells.filter((c) => c !== cell),
}
}, callback)
})
},
move_local_cells: (cells, new_index, callback = undefined) => {
set_notebook_state((prevstate) => {
move_local_cells: (cells, new_index) => {
return set_notebook_state((prevstate) => {
// The set of moved cell can be scatter across the notebook (not necessarily contiguous)
// but this action will move all of them to a single cluster
// The first cell of that cluster will be at index `new_index`.
Expand All @@ -134,7 +134,7 @@ export class Editor extends Component {
return {
cells: [...before, ...cells, ...after],
}
}, callback)
})
},
}

Expand Down Expand Up @@ -363,15 +363,53 @@ export class Editor extends Component {
wrap_remote_cell: (cell_id, block = "begin") => {
const cell = this.state.notebook.cells.find((c) => c.cell_id == cell_id)
const new_code = block + "\n\t" + cell.local_code.body.replace(/\n/g, "\n\t") + "\n" + "end"
set_cell_state(cell_id, {
remote_code: {
body: new_code,
submitted_by_me: false,
timestamp: Date.now(),
},
})
this.actions.update_local_cell_input(cell, false, new_code, cell.code_folded)
this.requests.change_remote_cell(cell_id, new_code)
},
split_remote_cell: async (cell_id, boundaries, submit = false) => {
const index = this.state.notebook.cells.findIndex((c) => c.cell_id == cell_id)
const cell = this.state.notebook.cells[index]

const old_code = cell.local_code.body
const padded_boundaries = [0, ...boundaries]
const parts = boundaries.map((b, i) => slice_utf8(old_code, padded_boundaries[i], b).trim()).filter((x) => x !== "")

const new_ids = []

// for loop because we need to wait for each addition to finish before adding the next, otherwise their order would be random
for (const [i, part] of parts.entries()) {
if (i === 0) {
new_ids.push(cell_id)
} else {
const update = await this.requests.add_remote_cell_at(index + i, true)
this.client.on_update(update, true)
new_ids.push(update.cell_id)
}
}

await Promise.all(
parts.map(async (part, i) => {
const id = new_ids[i]

// we set the cell's remote_code to force its value
await this.actions.update_local_cell_input({ cell_id: id }, false, part, false)

// we need to reset the remote_code, otherwise the cell will falsely report that it is in sync with the remote
const new_state = this.state.notebook.cells.find((c) => c.cell_id === id)
await this.set_cell_state(id, {
remote_code: {
...new_state.remote_code,
body: i === 0 ? old_code : "",
},
})
})
)

if (submit) {
const cells = new_ids.map((id) => this.state.notebook.cells.find((c) => c.cell_id == id))
await this.requests.set_and_run_multiple(cells)
}
},
interrupt_remote: (cell_id) => {
set_notebook_state((prevstate) => {
return {
Expand Down Expand Up @@ -425,19 +463,20 @@ export class Editor extends Component {
this.requests.add_remote_cell(cell_id, "after")
}
const index = this.state.notebook.cells.findIndex((c) => c.cell_id == cell_id)
const cell = this.state.notebook.cells[index]
this.setState({
recently_deleted: {
index: index,
body: this.state.notebook.cells[index].local_code.body,
},
})

set_cell_state(cell_id, {
running: true,
remote_code: {
body: "",
submitted_by_me: false,
},
}).then(() => {
this.actions.update_local_cell_input(cell, false, "", true)
})

this.client.send(
"delete_cell",
{},
Expand Down Expand Up @@ -814,7 +853,6 @@ export class Editor extends Component {
detail: {
cell_id: this.state.notebook.cells[new_i].cell_id,
line: delta === -1 ? Infinity : -1,
// ch: delta === -1 ? Infinity : -1,
},
})
)
Expand Down Expand Up @@ -865,7 +903,7 @@ export class Editor extends Component {
on_click=${() => {
this.requests.add_remote_cell_at(this.state.recently_deleted.index, true).then((update) => {
this.client.on_update(update, true)
this.actions.update_local_cell_input({ cell_id: update.cell_id }, false, this.state.recently_deleted.body, false, () => {
this.actions.update_local_cell_input({ cell_id: update.cell_id }, false, this.state.recently_deleted.body, false).then(() => {
this.requests.change_remote_cell(update.cell_id, this.state.recently_deleted.body)
})
})
Expand Down
44 changes: 34 additions & 10 deletions frontend/components/ErrorMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,40 @@ export const ErrorMessage = ({ msg, stacktrace, cell_id, requests }) => {
const rewriters = [
{
pattern: /syntax: extra token after end of expression/,
display: () =>
html`<p>Multiple expressions in one cell.</p>
<a
href="#"
onClick=${(e) => {
e.preventDefault()
requests.wrap_remote_cell(cell_id, "begin")
}}
>Wrap all code in a <em>begin ... end</em> block.</a
>`,
display: (x) => {
console.log(x)
const begin_hint = html`<a
href="#"
onClick=${(e) => {
e.preventDefault()
requests.wrap_remote_cell(cell_id, "begin")
}}
>Wrap all code in a <em>begin ... end</em> block.</a
>`
if (x.includes("\n\nBoundaries: ")) {
const boundaries = JSON.parse(x.split("\n\nBoundaries: ")[1]).map((x) => x - 1) // Julia to JS index
console.log(boundaries)
const split_hint = html`<p>
<a
href="#"
onClick=${(e) => {
e.preventDefault()
requests.split_remote_cell(cell_id, boundaries, true)
}}
>Split this cell into ${boundaries.length} cells</a
>, or
</p>`
return html`<p>Multiple expressions in one cell.</p>
<p>How would you like to fix it?</p>
<ul>
<li>${split_hint}</li>
<li>${begin_hint}</li>
</ul>`
} else {
return html`<p>Multiple expressions in one cell.</p>
<p>${begin_hint}</p>`
}
},
},
{
pattern: /LoadError: cannot assign a value to variable workspace\d+\..+ from module workspace\d+/,
Expand Down
Loading

4 comments on commit acc2117

@fonsp
Copy link
Owner Author

@fonsp fonsp commented on acc2117 Sep 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/20716

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.11.11 -m "<description of version>" acc2117baf9eead4f018452197841b299425dd44
git push origin v0.11.11

@fonsp
Copy link
Owner Author

@fonsp fonsp commented on acc2117 Sep 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#7

@fonsp
Copy link
Owner Author

@fonsp fonsp commented on acc2117 Sep 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.