From 7fe4e3e39dfb48427f0bfd90cfd32cdd9bd8ea2f Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Wed, 20 May 2026 00:04:18 +0200 Subject: [PATCH] Line wrapping: support spaces indenting --- frontend/components/CellInput.js | 24 ++++++-- .../CellInput/awesome_line_wrapping.js | 60 +++++++++++++------ 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index ebc107050e..acb0ac0d9b 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -48,7 +48,7 @@ import { markdown, html as htmlLang, javascript, sqlLang, python, julia_mixed } import { julia } from "../imports/CodemirrorPlutoSetup.js" import { pluto_autocomplete } from "./CellInput/pluto_autocomplete.js" import { NotebookpackagesFacet, pkgBubblePlugin } from "./CellInput/pkg_bubble_plugin.js" -import { awesome_line_wrapping, get_start_tabs } from "./CellInput/awesome_line_wrapping.js" +import { ARBITRARY_INDENT_LINE_WRAP_LIMIT, awesome_line_wrapping, get_leading_indent } from "./CellInput/awesome_line_wrapping.js" import { cell_movement_plugin, prevent_holding_a_key_from_doing_things_across_cells } from "./CellInput/cell_movement_plugin.js" import { pluto_paste_plugin } from "./CellInput/pluto_paste_plugin.js" import { bracketMatching } from "./CellInput/block_matcher_plugin.js" @@ -1130,17 +1130,29 @@ const InputContextMenuItem = ({ contents, title, onClick, setOpen, tag }) => ` +const generate_fake_deco_indent_text = (width) => { + const tab_size = 4 + const max_indent_ch = ARBITRARY_INDENT_LINE_WRAP_LIMIT * tab_size + if (width <= max_indent_ch) return " ".repeat(width) + + const left = width - max_indent_ch + return " ".repeat(max_indent_ch) + "⇥ ".repeat(Math.floor(left / 4)) + " ".repeat(left % 4) +} + const StaticCodeMirrorFaker = ({ value }) => { + const tab_size = 4 const lines = value.split("\n").map((line, i) => { - const start_tabs = get_start_tabs(line) + const { text: indent_text, width: indent_width } = get_leading_indent(line, tab_size) + const max_indent_ch = ARBITRARY_INDENT_LINE_WRAP_LIMIT * tab_size + const offset = Math.min(indent_width, max_indent_ch) const tabbed_line = - start_tabs.length == 0 + indent_text.length == 0 ? line - : html`${start_tabs}${line.substring(start_tabs.length)}` + : html`${generate_fake_deco_indent_text(indent_width)}${line.substring(indent_text.length)}` - return html`
+ return html`
${line.length === 0 ? html`
` : tabbed_line}
` }) diff --git a/frontend/components/CellInput/awesome_line_wrapping.js b/frontend/components/CellInput/awesome_line_wrapping.js index e10b0175b1..78579f4f44 100644 --- a/frontend/components/CellInput/awesome_line_wrapping.js +++ b/frontend/components/CellInput/awesome_line_wrapping.js @@ -3,21 +3,26 @@ import { StateField, EditorView, Decoration } from "../../imports/CodemirrorPlut import { ReactWidget } from "./ReactWidget.js" import { html } from "../../imports/Preact.js" -const ARBITRARY_INDENT_LINE_WRAP_LIMIT = 12 +export const ARBITRARY_INDENT_LINE_WRAP_LIMIT = 5 -export const get_start_tabs = (line) => /^\t*/.exec(line)?.[0] ?? "" +export const get_leading_indent = (line, tabSize) => { + const text = /^[\t ]*/.exec(line)?.[0] ?? "" + let width = 0 + for (const c of text) width += c === "\t" ? tabSize : 1 + return { text, width } +} const get_decorations = (/** @type {import("../../imports/CodemirrorPlutoSetup.js").EditorState} */ state) => { let decorations = [] + const max_indent_ch = ARBITRARY_INDENT_LINE_WRAP_LIMIT * state.tabSize // TODO? Don't create new decorations when a line hasn't changed? for (let i of _.range(0, state.doc.lines)) { let line = state.doc.line(i + 1) - const num_tabs = get_start_tabs(line.text).length - if (num_tabs === 0) continue + const { text: indent_text, width: indent_width } = get_leading_indent(line.text, state.tabSize) + if (indent_width === 0) continue - const how_much_to_indent = Math.min(num_tabs, ARBITRARY_INDENT_LINE_WRAP_LIMIT) - const offset = how_much_to_indent * state.tabSize + const offset = Math.min(indent_width, max_indent_ch) const linerwapper = Decoration.line({ attributes: { @@ -28,21 +33,38 @@ const get_decorations = (/** @type {import("../../imports/CodemirrorPlutoSetup.j // Need to push before the tabs one else codemirror gets madddd decorations.push(linerwapper.range(line.from, line.from)) - if (how_much_to_indent > 0) { - decorations.push( - Decoration.mark({ - class: "awesome-wrapping-plugin-the-tabs", - }).range(line.from, line.from + how_much_to_indent) - ) - } - if (num_tabs > how_much_to_indent) { - for (let i of _.range(how_much_to_indent, num_tabs)) { - decorations.push( - Decoration.replace({ + decorations.push( + Decoration.mark({ + class: "awesome-wrapping-plugin-the-tabs", + }).range(line.from, line.from + indent_text.length) + ) + + // For indent past the cap, replace remaining tabs with a faded ⇥ widget. + if (indent_width > max_indent_ch) { + let acc = 0 + + for (let j = 0; j < indent_text.length; j++) { + const c = indent_text[j] + const w = c === "\t" ? state.tabSize : 1 + if (acc >= max_indent_ch) { + const deco = Decoration.replace({ widget: new ReactWidget(html``), block: false, - }).range(line.from + i, line.from + i + 1) - ) + }) + + if (c === "\t") { + decorations.push(deco.range(line.from + j, line.from + j + 1)) + } else if (c === " ") { + // If 4 spaces are coming up... + if (" ".repeat(state.tabSize) === indent_text.slice(j, j + state.tabSize)) { + // ...then replace with single deco. + decorations.push(deco.range(line.from + j, line.from + j + state.tabSize)) + // Skip to next indent unit + j += state.tabSize - 1 + } + } + } + acc += w } } }