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
}
}
}