From 6746395a7fc870ea24406526459a71783c95d89d Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 19 Apr 2024 23:11:34 +0200 Subject: [PATCH] Use `ch` CSS unit to simplify awesome_line_wrapping CM plugin (#2899) --- frontend/components/CellInput.js | 5 +- .../CellInput/awesome_line_wrapping.js | 198 ++---------------- 2 files changed, 18 insertions(+), 185 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index cb4bf1bcb7..2e09e865b2 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -46,7 +46,7 @@ import { markdown, html as htmlLang, javascript, sqlLang, python, julia_mixed } import { julia_andrey } 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 } from "./CellInput/awesome_line_wrapping.js" +import { awesome_line_wrapping, get_start_tabs } 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" @@ -804,7 +804,6 @@ export const CellInput = ({ EditorView.contentAttributes.of({ spellcheck: String(ENABLE_CM_SPELLCHECK) }), EditorView.lineWrapping, - // Wowww this has been enabled for some time now... wonder if there are issues about this yet ;) - DRAL awesome_line_wrapping, // Reset diagnostics on change @@ -1126,7 +1125,7 @@ const InputContextMenuItem = ({ contents, title, onClick, setOpen, tag }) => const StaticCodeMirrorFaker = ({ value }) => { const lines = value.split("\n").map((line, i) => { - const start_tabs = /^\t*/.exec(line)?.[0] ?? "" + const start_tabs = get_start_tabs(line) const tabbed_line = start_tabs.length == 0 diff --git a/frontend/components/CellInput/awesome_line_wrapping.js b/frontend/components/CellInput/awesome_line_wrapping.js index 8abf500853..8f4ab2a536 100644 --- a/frontend/components/CellInput/awesome_line_wrapping.js +++ b/frontend/components/CellInput/awesome_line_wrapping.js @@ -1,123 +1,54 @@ import _ from "../../imports/lodash.js" -import { StateEffect, StateField, EditorView, Decoration } from "../../imports/CodemirrorPlutoSetup.js" +import { StateField, EditorView, Decoration } from "../../imports/CodemirrorPlutoSetup.js" import { ReactWidget } from "./ReactWidget.js" import { html } from "../../imports/Preact.js" +const ARBITRARY_INDENT_LINE_WRAP_LIMIT = 12 + +export const get_start_tabs = (line) => /^\t*/.exec(line)?.[0] ?? "" + /** * Plugin that makes line wrapping in the editor respect the identation of the line. * It does this by adding a line decoration that adds padding-left (as much as there is indentation), * and adds the same amount as negative "text-indent". The nice thing about text-indent is that it * applies to the initial line of a wrapped line. - * - * The identation decorations have to happen in a StateField (without access to the editor), - * because they change the layout of the text :( The character width I need however, is in the editor... - * So I do this ugly hack where I, in `character_width_listener`, I fire an effect that gets picked up - * by another StateField (`extra_cycle_character_width`) that saves the character width into state, - * so THEN I can add the markers in the decorations statefield. */ - -/** @type {any} */ -const CharacterWidthEffect = StateEffect.define({}) -const extra_cycle_character_width = StateField.define({ - create() { - return { defaultCharacterWidth: null, measuredSpaceWidth: null, measuredTabWidth: null } - }, - update(value, tr) { - for (let effect of tr.effects) { - if (effect.is(CharacterWidthEffect)) return effect.value - } - return value - }, -}) - -let character_width_listener = EditorView.updateListener.of((viewupdate) => { - let width = viewupdate.view.defaultCharacterWidth - let { defaultCharacterWidth, measuredSpaceWidth } = viewupdate.view.state.field(extra_cycle_character_width, false) - - // I assume that codemirror will notice if text size changes, - // so only then I'll also re-measure the space width. - if (defaultCharacterWidth !== width) { - // Tried to adapt so it would always use the dummy line (with just spaces), but it never seems to work - // https://github.com/codemirror/view/blob/41eaf3e1435ec62ecb128f7e4b8d4df2a02140db/src/docview.ts#L324-L343 - // I guess best to first fix defaultCharacterWidth in CM6, - // but eventually we'll need a way to actually measures the identation of the line. - // Hopefully this person will respond: - // https://discuss.codemirror.net/t/custom-dom-inline-styles/3563/10 - let space_width - let tab_width - // @ts-ignore - - viewupdate.view.dispatch({ - effects: [ - CharacterWidthEffect.of({ - defaultCharacterWidth: width, - measuredSpaceWidth: space_width, - measuredTabWidth: tab_width, - }), - ], - }) - } -}) - -let ARBITRARY_INDENT_LINE_WRAP_LIMIT = 12 -let line_wrapping_decorations = StateField.define({ +export const awesome_line_wrapping = StateField.define({ create() { return Decoration.none }, update(deco, tr) { - // let tabSize = tr.state.tabSize - let tabSize = 4 - let previous = tr.startState.field(extra_cycle_character_width, false) - let previous_space_width = previous.measuredSpaceWidth ?? previous.defaultCharacterWidth - let { measuredSpaceWidth, defaultCharacterWidth } = tr.state.field(extra_cycle_character_width, false) - let space_width = measuredSpaceWidth ?? defaultCharacterWidth - - if (space_width == null) return Decoration.none - if (!tr.docChanged && deco !== Decoration.none && previous_space_width === space_width) return deco + if (!tr.docChanged && deco !== Decoration.none) return deco let decorations = [] - // TODO? Only apply to visible lines? Wouldn't that screw stuff up?? // TODO? Don't create new decorations when a line hasn't changed? for (let i of _.range(0, tr.state.doc.lines)) { let line = tr.state.doc.line(i + 1) - if (line.length === 0) continue + const num_tabs = get_start_tabs(line.text).length + if (num_tabs === 0) continue - let indented_tabs = 0 - for (let ch of line.text) { - if (ch === "\t") { - indented_tabs++ - // For now I ignore spaces... because they are weird... and stupid! - // } else if (ch === " ") { - // indented_chars = indented_chars + 1 - // indented_text_characters++ - } else { - break - } - } - - const characters_to_count = Math.min(indented_tabs, ARBITRARY_INDENT_LINE_WRAP_LIMIT) - const offset = characters_to_count * tabSize * space_width + const how_much_to_indent = Math.min(num_tabs, ARBITRARY_INDENT_LINE_WRAP_LIMIT) + const offset = how_much_to_indent * tr.state.tabSize const linerwapper = Decoration.line({ attributes: { - // style: rules.cssText, - style: `--indented: ${offset}px;`, + style: `--indented: ${offset}ch;`, class: "awesome-wrapping-plugin-the-line", }, }) // Need to push before the tabs one else codemirror gets madddd decorations.push(linerwapper.range(line.from, line.from)) - if (characters_to_count !== 0) { + if (how_much_to_indent > 0) { decorations.push( Decoration.mark({ class: "awesome-wrapping-plugin-the-tabs", - }).range(line.from, line.from + characters_to_count) + }).range(line.from, line.from + how_much_to_indent) ) } - if (indented_tabs > characters_to_count) { - for (let i of _.range(characters_to_count, indented_tabs)) { + if (num_tabs > how_much_to_indent) { + for (let i of _.range(how_much_to_indent, num_tabs)) { decorations.push( Decoration.replace({ widget: new ReactWidget(html``), @@ -126,105 +57,8 @@ let line_wrapping_decorations = StateField.define({ ) } } - - // let tabs_in_front = Math.min(line.text.match(/^\t*/)[0].length) * tabSize - - // TODO? Cache the CSSStyleDeclaration? - // This is used when we don't use a css class, but we do need a css class because - // text-indent normally cascades, and we have to prevent that. - // const rules = document.createElement("span").style - // rules.setProperty("--idented", `${offset}px`) - // rules.setProperty("text-indent", "calc(-1 * var(--idented) - 1px)") // I have no idea why, but without the - 1px it behaves weirdly periodically - // rules.setProperty("padding-left", "calc(var(--idented) + var(--cm-left-padding, 4px))") } return Decoration.set(decorations) }, provide: (f) => EditorView.decorations.from(f), }) - -// Add this back in -// let dont_break_before_spaces_matcher = new MatchDecorator({ -// regexp: /[^ \t]+[ \t]+/g, -// decoration: Decoration.mark({ -// class: "indentation-so-dont-break", -// }), -// }) - -let identation_so_dont_break_marker = Decoration.mark({ - class: "indentation-so-dont-break", -}) - -let dont_break_before_spaces = StateField.define({ - create() { - return Decoration.none - }, - update(deco, tr) { - let decorations = [] - let pos = 0 - for (const line of tr.newDoc) { - for (const match of /** @type{string} */ (line).matchAll(/[^ \t]+([ \t]|$)+/g)) { - if (match.index == null || match.index === 0) continue // Sneaky negative lookbehind - decorations.push(identation_so_dont_break_marker.range(pos + match.index, pos + match.index + match[0].length)) - } - } - return Decoration.set(decorations, true) - }, - provide: (f) => EditorView.decorations.from(f), -}) - -// let break_after_space_matcher = new MatchDecorator({ -// regexp: /[ ](?=[^ \t])/g, -// decoration: Decoration.widget({ -// widget: new ReactWidget(html` `), -// block: false, -// side: 1, -// }), -// }) - -// let break_after_space = ViewPlugin.define( -// (view) => { -// return { -// decorations: break_after_space_matcher.createDeco(view), -// update(update) { -// this.decorations = break_after_space_matcher.updateDeco(update, this.decorations) -// }, -// } -// }, -// { -// decorations: (v) => v.decorations, -// } -// ) - -// let dont_break_start_of_line_matcher = new MatchDecorator({ -// regexp: /^[ \t]+[^ \t]/g, -// decoration: Decoration.mark({ -// class: "Uhhh", -// }), -// }) - -// let dont_break_start_of_line = ViewPlugin.define( -// (view) => { -// return { -// decorations: dont_break_start_of_line_matcher.createDeco(view), -// update(update) { -// this.decorations = dont_break_start_of_line_matcher.updateDeco(update, this.decorations) -// }, -// } -// }, -// { -// decorations: (v) => v.decorations, -// } -// ) - -// console.log(`awesome_line_wrapping:`, indent_decorations) -export let awesome_line_wrapping = [ - // dont_break_start_of_line, - extra_cycle_character_width, - character_width_listener, - line_wrapping_decorations, - // break_after_space, - // dont_break_before_spaces, -] -// export let awesome_line_wrapping = [] -// export let awesome_line_wrapping = indent_decorations -// export let awesome_line_wrapping = [dont_break_before_spaces, break_after_space]