Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐭 Ctrl+drag numbers in code to change value #3090

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading