Skip to content

Commit

Permalink
🐭 Ctrl+drag numbers in code to change value
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsp committed Nov 8, 2024
1 parent f5499e3 commit ee64d69
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 10 deletions.
13 changes: 10 additions & 3 deletions frontend/components/CellInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { LastFocusWasForcedEffect, tab_help_plugin } from "./CellInput/tab_help_
import { useEventListener } from "../common/useEventListener.js"
import { moveLineDown } from "../imports/CodemirrorPlutoSetup.js"
import { is_mac_keyboard } from "../common/KeyboardShortcuts.js"
import { checkboxPlugin } from "./CellInput/number_dragger_plugin.js"

export const ENABLE_CM_MIXED_PARSER = window.localStorage.getItem("ENABLE_CM_MIXED_PARSER") === "true"
export const ENABLE_CM_SPELLCHECK = window.localStorage.getItem("ENABLE_CM_SPELLCHECK") === "true"
Expand Down Expand Up @@ -365,12 +366,12 @@ export const CellInput = ({
return true
}

const anySelect = cm.state.selection.ranges.some(r => !r.empty)
const anySelect = cm.state.selection.ranges.some((r) => !r.empty)
if (anySelect) {
return indentMore(cm)
} else {
cm.dispatch(
cm.state.changeByRange(selection => ({
cm.dispatch(
cm.state.changeByRange((selection) => ({
range: EditorSelection.cursor(selection.from + 1),
changes: { from: selection.from, to: selection.to, insert: "\t" },
}))
Expand Down Expand Up @@ -702,6 +703,12 @@ export const CellInput = ({
EditorView.lineWrapping,
awesome_line_wrapping,

checkboxPlugin({
run_cell: () => {
on_submit()
},
}),

// Reset diagnostics on change
EditorView.updateListener.of((update) => {
if (!update.docChanged) return
Expand Down
188 changes: 188 additions & 0 deletions frontend/components/CellInput/number_dragger_plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { render } from "../../imports/Preact.js"
import { EditorView, WidgetType, ViewUpdate, ViewPlugin, syntaxTree, Decoration } from "../../imports/CodemirrorPlutoSetup.js"
import { has_ctrl_or_cmd_pressed } from "../../common/KeyboardShortcuts.js"

class CheckboxWidget extends WidgetType {
checked

constructor(checked) {
super()
this.checked = checked
}

eq(other) {
return other.checked == this.checked
}

toDOM() {
let wrap = document.createElement("span")
wrap.setAttribute("aria-hidden", "true")
wrap.className = "cm-boolean-toggle"
let box = wrap.appendChild(document.createElement("input"))
box.type = "checkbox"
box.checked = this.checked
return wrap
}

ignoreEvent() {
return false
}
}

const magic_number_class = "magic-number-yay"

/**
* @param {EditorView} view
*/
function checkboxes(view) {
let widgets = []
for (let { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: (node) => {
if (node.name === "BooleanLiteral") {
let isTrue = view.state.doc.sliceString(node.from, node.to) === "true"
let deco = Decoration.replace({
widget: new CheckboxWidget(isTrue),
// side: 1,
})
widgets.push(deco.range(node.from, node.to))
}
},
})
}

for (let { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: (node) => {
if (node.name === "Number") {
let str = view.state.doc.sliceString(node.from, node.to)
if (!julia_number_supported(str)) return
let deco = Decoration.mark({
class: magic_number_class,
attributes: { "data-magic-number": "yes" },
})
widgets.push(deco.range(node.from, node.to))
}
},
})
}

return Decoration.set(widgets)
}

const julia_number_supported = (str) => {
return str.match(/^\d+(\.\d+)?$/) != null
}

const julia_string_to_number = (str) => {
return parseFloat(str)
}

const dragged_value = (start_string, delta) => {
const is_float = start_string.includes(".")

return Math.round(julia_string_to_number(start_string) + delta * 0.3).toString()
}

export const checkboxPlugin = ({ run_cell }) => {
let dragging = false
/** @type {any} */
let node = null
/** @type {PointerEvent?} */
let drag_start_event = null

let start_str = "3.14"

return ViewPlugin.fromClass(
class {
decorations

constructor(view) {
this.decorations = checkboxes(view)
}

update(update) {
if (update.docChanged || update.viewportChanged || syntaxTree(update.startState) != syntaxTree(update.state))
this.decorations = checkboxes(update.view)
}
},
{
decorations: (v) => v.decorations,

eventHandlers: {
mousedown: (e, view) => {
let target = e.target
if (target instanceof HTMLElement && target.nodeName == "INPUT" && target.parentElement?.classList?.contains?.("cm-boolean-toggle"))
return toggleBoolean(view, view.posAtDOM(target))
},

pointerdown: (e, view) => {
console.log(e)
if (!has_ctrl_or_cmd_pressed(e)) return
const target = e.target
if (!(target instanceof HTMLElement)) return
const mn = target.closest(`.${magic_number_class}`)
if (mn == null) return

const pos = view.posAtDOM(mn)
node = syntaxTree(view.state).resolve(pos, 1)
drag_start_event = e

start_str = view.state.doc.sliceString(node.from, node.to)

if (!julia_number_supported(start_str)) return

console.log({ pos, node, start_str })

dragging = true
return true
},

pointerup: (e, view) => {
dragging = false
},

pointerleave: (e, view) => {
dragging = false
},

pointermove: (e, view) => {
if (!dragging || drag_start_event == null) return

const delta = drag_start_event.clientY - e.clientY

const new_str = dragged_value(start_str, delta)
const current_str = view.state.doc.sliceString(node.from, node.to)
if (new_str === current_str) return

view.dispatch({
changes: { from: node.from, to: node.to, insert: new_str },
})
// the string length might have changed, so we need to re-resolve the node
node = syntaxTree(view.state).resolve(node.from, 1)

// run the cell with this new code
run_cell()
},
},
}
)
}

/**
* @param {EditorView} view
* @param {number} pos
*/
function toggleBoolean(view, pos) {
let before = view.state.doc.sliceString(Math.max(0, pos - 5), pos)
let change
if (before == "false") change = { from: pos - 5, to: pos, insert: "true" }
else if (before.endsWith("true")) change = { from: pos - 4, to: pos, insert: "false" }
else return false
view.dispatch({ changes: change })
return true
}
4 changes: 2 additions & 2 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1265,8 +1265,8 @@ all patches: ${JSON.stringify(patches, null, 1)}
const set_ctrl_down = (value) => {
if (value !== ctrl_down_last_val.current) {
ctrl_down_last_val.current = value
document.body.querySelectorAll("[data-pluto-variable], [data-cell-variable]").forEach((el) => {
el.setAttribute("data-ctrl-down", value ? "true" : "false")
document.body.querySelectorAll("[data-pluto-variable], [data-cell-variable], [data-magic-number]").forEach((el) => {
el.closest("pluto-cell").setAttribute("data-ctrl-down", value ? "true" : "false")
})
}
}
Expand Down
17 changes: 12 additions & 5 deletions frontend/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -3401,13 +3401,13 @@ body.disable_ui [data-pluto-variable],
body.disable_ui [data-cell-variable] {
cursor: pointer;
}
body:not(.disable_ui) [data-ctrl-down="true"][data-pluto-variable],
body:not(.disable_ui) [data-ctrl-down="true"][data-cell-variable] {
body:not(.disable_ui) [data-ctrl-down="true"] [data-pluto-variable],
body:not(.disable_ui) [data-ctrl-down="true"] [data-cell-variable] {
text-decoration-color: #d177e6;
cursor: pointer;
}
body:not(.disable_ui) [data-ctrl-down="true"][data-pluto-variable]:hover,
body:not(.disable_ui) [data-ctrl-down="true"][data-pluto-variable]:hover * {
body:not(.disable_ui) [data-ctrl-down="true"] [data-pluto-variable]:hover,
body:not(.disable_ui) [data-ctrl-down="true"] [data-pluto-variable]:hover * {
/* This basically `color: #af5bc3`, but it works for emoji too!! */
color: transparent !important;
text-shadow: 0 0 #af5bc3;
Expand All @@ -3418,12 +3418,19 @@ body:not(.disable_ui) [data-ctrl-down="true"][data-pluto-variable]:hover * {
/* Can give this cool styles later as well, but not for now nahhh */
text-decoration: none;
}
[data-ctrl-down="true"][data-cell-variable]:hover * {
[data-ctrl-down="true"] [data-cell-variable]:hover * {
/* This basically `color: #af5bc3`, but it works for emoji too!! */
color: transparent !important;
text-shadow: 0 0 #af5bc3;
}

[data-ctrl-down="true"] [data-magic-number] {
cursor: ns-resize;
outline: 2px solid pink;
outline-offset: 1px;
border-radius: 3px;
}

.cm-tooltip.cm-tooltip-autocomplete {
padding: 0;
margin-left: -1.5em;
Expand Down

0 comments on commit ee64d69

Please sign in to comment.