Skip to content

Commit

Permalink
Use ch CSS unit to simplify awesome_line_wrapping CM plugin (#2899)
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsp authored Apr 19, 2024
1 parent 3b0b050 commit 6746395
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 185 deletions.
5 changes: 2 additions & 3 deletions frontend/components/CellInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
198 changes: 16 additions & 182 deletions frontend/components/CellInput/awesome_line_wrapping.js
Original file line number Diff line number Diff line change
@@ -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`<span style=${{ opacity: 0.2 }}></span>`),
Expand All @@ -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` <wbr></wbr>`),
// 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]

0 comments on commit 6746395

Please sign in to comment.