Skip to content

Commit

Permalink
⌨ More keyboard shortcuts (#334)
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsp authored Sep 2, 2020
1 parent 183ec96 commit 289b960
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 67 deletions.
2 changes: 1 addition & 1 deletion frontend/common/Feedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const finalize_statistics = async (state, client, counter_statistics) =>
// timestamp (ms)
screenWidthApprox: 100 * Math.round(document.body.clientWidth / 100),
// number, rounded to nearest multiple of 100
docsOpen: parseFloat(window.getComputedStyle(document.querySelector("helpbox")).height) > 200,
docsOpen: parseFloat(window.getComputedStyle(document.querySelector("pluto-helpbox")).height) > 200,
// bool
hasFocus: document.hasFocus(),
// bool
Expand Down
10 changes: 1 addition & 9 deletions frontend/components/Cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,7 @@ export const Cell = ({
}}
on_delete=${() => {
const friends = selected_friends(cell_id)
if (friends.length == 1 || confirm(`Delete ${friends.length} cells?`)) {
if (friends.some((f) => f.running)) {
if (confirm("This cell is still running - would you like to interrupt the notebook?")) {
requests.interrupt_remote(cell_id)
}
} else {
friends.forEach((f) => requests.delete_cell(f.cell_id))
}
}
requests.confirm_delete_multiple(friends)
}}
on_add_after=${() => {
requests.add_remote_cell(cell_id, "after")
Expand Down
140 changes: 117 additions & 23 deletions frontend/components/CellInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const clear_selection = (cm) => {
cm.setSelection(c, c, { scroll: false })
}

const last = (x) => x[x.length - 1]
const all_equal = (x) => x.every((y) => y === x[0])

export const CellInput = ({
is_hidden,
remote_code,
Expand All @@ -27,9 +30,14 @@ export const CellInput = ({
}) => {
const cm_ref = useRef(null)
const dom_node_ref = useRef(null)
const remote_code_ref = useRef(null)
const change_handler_ref = useRef(null)
change_handler_ref.current = on_change

useEffect(() => {
remote_code_ref.current = remote_code
}, [remote_code])

useEffect(() => {
const cm = (cm_ref.current = window.CodeMirror(
(el) => {
Expand All @@ -54,29 +62,115 @@ export const CellInput = ({
}
))

cm.setOption("extraKeys", {
"Shift-Enter": () => on_submit(cm.getValue()),
"Ctrl-Enter": () => {
on_add_after()
// on_fold(true)
on_submit(cm.getValue())
},
"Shift-Delete": () => {
if (confirm("Delete cell?")) {
on_delete()
const mac_keyboard = /Mac/.test(navigator.platform)

const keys = {}

keys["Shift-Enter"] = () => on_submit(cm.getValue())
keys["Ctrl-Enter"] = () => {
on_add_after()

const new_value = cm.getValue()
console.log(new_value)
console.log(remote_code_ref.current.body)
if (new_value !== remote_code_ref.current.body) {
on_submit(new_value)
}
}
// these should be fn+Up and fn+Down on recent apple keyboards
// please confirm and change this comment <3
keys["PageUp"] = () => {
on_focus_neighbor(cell_id, -1)
}
keys["PageDown"] = () => {
on_focus_neighbor(cell_id, +1)
}
keys["Shift-Tab"] = "indentLess"
keys["Tab"] = on_tab_key
keys[mac_keyboard ? "Cmd-D" : "Ctrl-D"] = () => {
if (cm.somethingSelected()) {
const sels = cm.getSelections()
if (all_equal(sels)) {
// TODO
}
},
// these should be fn+Up and fn+Down on recent apple keyboards
// please confirm and change this comment <3
"PageUp": () => {
} else {
const cursor = cm.getCursor()
const token = cm.getTokenAt(cursor)
console.log(cursor)
cm.setSelection({ line: cursor.line, ch: token.start }, { line: cursor.line, ch: token.end })
console.log(token)
}
}
keys[mac_keyboard ? "Cmd-/" : "Ctrl-/"] = () => {
const old_value = cm.getValue()
cm.toggleComment()
const new_value = cm.getValue()
if (old_value === new_value) {
// the commenter failed for some reason
// this happens when lines start with `md"`, with no indent
cm.setValue(cm.lineCount() === 1 ? `# ${new_value}` : `#= ${new_value} =#`)
cm.execCommand("selectAll")
}
}
const swap = (a, i, j) => {
;[a[i], a[j]] = [a[j], a[i]]
}
const range = (a, b) => {
const x = Math.min(a, b)
const y = Math.max(a, b)
return [...Array(y + 1 - x).keys()].map((i) => i + x)
}
const alt_move = (delta) => {
const selections = cm.listSelections()
const selected_lines = new Set([].concat(...selections.map((sel) => range(sel.anchor.line, sel.head.line))))
const final_line_number = delta === 1 ? cm.lineCount() - 1 : 0
if (!selected_lines.has(final_line_number)) {
Array.from(selected_lines)
.sort((a, b) => delta * a < delta * b)
.forEach((line_number) => {
const lines = cm.getValue().split("\n")
swap(lines, line_number, line_number + delta)
cm.setValue(lines.join("\n"))
cm.indentLine(line_number + delta, "smart")
cm.indentLine(line_number, "smart")
})
cm.setSelections(
selections.map((sel) => {
return {
head: {
line: sel.head.line + delta,
ch: sel.head.ch,
},
anchor: {
line: sel.anchor.line + delta,
ch: sel.anchor.ch,
},
}
})
)
}
}
keys["Alt-Up"] = () => alt_move(-1)
keys["Alt-Down"] = () => alt_move(+1)

keys["Backspace"] = keys[mac_keyboard ? "Cmd-Backspace" : "Ctrl-Backspace"] = () => {
if (cm.lineCount() === 1 && cm.getValue() === "") {
on_focus_neighbor(cell_id, -1)
},
"PageDown": () => {
on_delete()
console.log("backspace!")
}
return window.CodeMirror.Pass
}
keys["Delete"] = keys[mac_keyboard ? "Cmd-Delete" : "Ctrl-Delete"] = () => {
if (cm.lineCount() === 1 && cm.getValue() === "") {
on_focus_neighbor(cell_id, +1)
},
"Shift-Tab": "indentLess",
"Tab": on_tab_key,
})
on_delete()
console.log("delete!")
}
return window.CodeMirror.Pass
}

cm.setOption("extraKeys", keys)

cm.on("cursorActivity", () => {
if (cm.somethingSelected()) {
Expand All @@ -87,9 +181,9 @@ export const CellInput = ({
}
} else {
const token = cm.getTokenAt(cm.getCursor())
if(token.start === 0 && token.type === "operator" && token.string === "?"){
if (token.start === 0 && token.type === "operator" && token.string === "?") {
// https://github.com/fonsp/Pluto.jl/issues/321
const second_token = cm.getTokenAt({...cm.getCursor(), ch: 2})
const second_token = cm.getTokenAt({ ...cm.getCursor(), ch: 2 })
on_update_doc_query(second_token.string)
} else if (token.type != null && token.type !== "string") {
on_update_doc_query(token.string)
Expand All @@ -99,7 +193,7 @@ export const CellInput = ({

cm.on("change", () => {
const new_value = cm.getValue()
if(new_value.length > 1 && new_value[0] === "?"){
if (new_value.length > 1 && new_value[0] === "?") {
window.dispatchEvent(new CustomEvent("open_live_docs"))
}
change_handler_ref.current(new_value)
Expand Down
42 changes: 32 additions & 10 deletions frontend/components/DropRuler.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,27 @@ export class DropRuler extends Component {
this.cell_edges = cell_nodes.map((el) => el.offsetTop)
this.cell_edges.push(last(cell_nodes).offsetTop + last(cell_nodes).scrollHeight)
}
this.getDropIndexOf = (pageY) => {
const distances = this.cell_edges.map((p) => Math.abs(p - pageY - 8)) // 8 is the magic computer number: https://en.wikipedia.org/wiki/8
this.getDropIndexOf = ({ pageX, pageY }, always_round_nearest = false) => {
const notebook = document.querySelector("pluto-notebook")

const rounding_mode = always_round_nearest
? "nearest"
: pageX < notebook.offsetLeft
? "floor"
: pageX > notebook.offsetLeft + notebook.scrollWidth
? "ceil"
: "nearest"

const f =
rounding_mode === "ceil"
? (x) => (x >= 0 ? x : Infinity)
: rounding_mode === "floor"
? (x) => (x <= 0 ? -x : Infinity)
: rounding_mode === "nearest"
? Math.abs
: Math.abs

const distances = this.cell_edges.map((p) => f(p - pageY - 8)) // 8 is the magic computer number: https://en.wikipedia.org/wiki/8
return argmin(distances)
}

Expand All @@ -35,19 +54,20 @@ export class DropRuler extends Component {
})
this.dropee = null
} else {
this.dropee = e.target.parentElement
this.precompute_cell_edges()

this.setState({
dragging: true,
drop_index: this.getDropIndexOf(e, true),
})
this.dropee = e.target.parentElement

this.precompute_cell_edges()
}
})

document.addEventListener("dragover", (e) => {
// Called continuously during drag
this.setState({
drop_index: this.getDropIndexOf(e.pageY),
drop_index: this.getDropIndexOf(e, true),
})
e.preventDefault()
})
Expand All @@ -62,17 +82,19 @@ export class DropRuler extends Component {
return
}
// Called when drag-dropped somewhere on the page
const drop_index = this.getDropIndexOf(e.pageY)
const drop_index = this.getDropIndexOf(e, true)
const friends = this.props.selected_friends(this.dropee.id)
this.props.requests.move_remote_cells(friends, drop_index)
})

/* SELECTIONS */

document.addEventListener("mousedown", (e) => {
if (e.button === 0 && (e.target.tagName === "MAIN" || e.target.tagName === "PLUTO-NOTEBOOK" || e.target.tagName === "PREAMBLE")) {
const t = e.target.tagName
// TODO: also allow starting the selection in one codemirror and stretching it to another cell
if (e.button === 0 && (t === "BODY" || t === "MAIN" || t === "PLUTO-NOTEBOOK" || t === "PREAMBLE")) {
this.precompute_cell_edges()
const new_index = this.getDropIndexOf(e.pageY)
const new_index = this.getDropIndexOf(e)
this.setState({
selecting: true,
selection_start_index: new_index,
Expand Down Expand Up @@ -113,7 +135,7 @@ export class DropRuler extends Component {

document.addEventListener("mousemove", (e) => {
if (this.state.selecting) {
const new_stop_index = this.getDropIndexOf(e.pageY)
const new_stop_index = this.getDropIndexOf(e)
if (new_stop_index !== this.state.selection_stop_index) {
this.setState({
selection_stop_index: new_stop_index,
Expand Down
27 changes: 22 additions & 5 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,17 @@ export class Editor extends Component {
false
)
},
confirm_delete_multiple: (cells) => {
if (cells.length <= 1 || confirm(`Delete ${cells.length} cells?`)) {
if (cells.some((f) => f.running)) {
if (confirm("This cell is still running - would you like to interrupt the notebook?")) {
this.requests.interrupt_remote(cells[0].cell_id)
}
} else {
cells.forEach((f) => this.requests.delete_cell(f.cell_id))
}
}
},
fold_remote_cell: (cell_id, newFolded) => {
this.client.send(
"fold_cell",
Expand Down Expand Up @@ -611,6 +622,13 @@ export class Editor extends Component {
e.preventDefault()
}
break
case 8: // backspace
// fall into:
case 46: // delete
const selected = this.state.notebook.cells.filter((c) => c.selected)
this.requests.confirm_delete_multiple(selected)
e.preventDefault()
break
case 191: // ? or /
if (!(e.ctrlKey && e.shiftKey)) {
break
Expand All @@ -623,7 +641,7 @@ export class Editor extends Component {
Shift+Enter: run cell
Ctrl+Enter: run cell and add cell below
Shift+Delete: delete cell
Delete or Backspace: delete empty cell
PageUp or fn+Up: select cell above
PageDown or fn+Down: select cell below
Expand Down Expand Up @@ -699,9 +717,7 @@ export class Editor extends Component {
<header>
<aside id="export">
<div id="container">
<div class="export_title">
export
</div>
<div class="export_title">export</div>
<a href="./notebookfile?id=${this.state.notebook.notebook_id}" target="_blank" class="export_card">
<header>${triangle("#a270ba")} Notebook file</header>
<section>Download a copy of the <b>.jl</b> script.</section>
Expand Down Expand Up @@ -797,7 +813,8 @@ export class Editor extends Component {
new CustomEvent("cell_focus", {
detail: {
cell_id: this.state.notebook.cells[new_i].cell_id,
line: -1,
line: delta === -1 ? Infinity : -1,
// ch: delta === -1 ? Infinity : -1,
},
})
)
Expand Down
6 changes: 3 additions & 3 deletions frontend/components/LiveDocs.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class LiveDocs extends Component {
shown_query: null,
searched_query: null,
body: "Start typing in a cell to learn more!",
hidden: false,
hidden: true,
loading: false,
}
this.updateDocTimer = undefined
Expand Down Expand Up @@ -75,7 +75,7 @@ export class LiveDocs extends Component {
render() {
return html`
<aside id="helpbox-wrapper">
<helpbox class=${cl({ hidden: this.state.hidden, loading: this.state.loading })}>
<pluto-helpbox class=${cl({ hidden: this.state.hidden, loading: this.state.loading })}>
<header onClick=${() => this.setState({ hidden: !this.state.hidden })}>
${this.state.hidden || this.state.searched_query == null ? "Live docs" : this.state.searched_query}
</header>
Expand All @@ -87,7 +87,7 @@ export class LiveDocs extends Component {
on_render=${(n) => resolve_doc_reference_links(n, this.props.on_update_doc_query)}
/>
</section>
</helpbox>
</pluto-helpbox>
</aside>
`
}
Expand Down
Loading

1 comment on commit 289b960

@fonsp
Copy link
Owner Author

@fonsp fonsp commented on 289b960 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.