| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- import {
- Decoration,
- DecorationSet,
- EditorView,
- ViewPlugin,
- ViewUpdate,
- } from '@codemirror/view'
- import { type Range, StateEffect, StateField } from '@codemirror/state'
- import { sourceOnly } from './visual/visual'
- const MAX_INDENT_FRACTION = 0.9
- const setMaxIndentEffect = StateEffect.define<number>()
- /**
- * Calculates the indentation needed for each line based on the spaces and tabs at the start of the line.
- * Decorates each line with a `style` attribute, to ensure that wrapped lines share the same indentation.
- *
- * NOTE: performance increases linearly with document size, but decorating only the viewport causes unacceptable layout shifts when scrolling.
- */
- export const lineWrappingIndentation = (visual: boolean) => {
- // this extension should only be active in the source editor
- return sourceOnly(visual, [
- // store the current maxIndent, based on the clientWidth
- StateField.define<number>({
- create() {
- return 0
- },
- update(value, tr) {
- for (const effect of tr.effects) {
- if (effect.is(setMaxIndentEffect)) {
- value = effect.value
- }
- }
- return value
- },
- provide(field) {
- return [
- // calculate the max indent when the geometry changes
- ViewPlugin.define(view => {
- const measure = {
- key: 'line-wrapping-indentation-max-indent',
- read(view: EditorView) {
- return (
- (view.contentDOM.clientWidth / view.defaultCharacterWidth) *
- MAX_INDENT_FRACTION
- )
- },
- write(value: number, view: EditorView) {
- if (view.state.field(field) !== value) {
- window.setTimeout(() => {
- view.dispatch({
- effects: setMaxIndentEffect.of(value),
- })
- })
- }
- },
- }
- return {
- update(update: ViewUpdate) {
- if (update.geometryChanged) {
- view.requestMeasure(measure)
- }
- },
- }
- }),
- // rebuild the decorations when needed
- ViewPlugin.define<{ decorations: DecorationSet }>(
- view => {
- let previousMaxIndent = 0
- const value = {
- decorations: buildDecorations(view, view.state.field(field)),
- update(update: ViewUpdate) {
- const maxIndent = view.state.field(field)
- if (
- maxIndent !== previousMaxIndent ||
- update.geometryChanged ||
- update.viewportChanged
- ) {
- value.decorations = buildDecorations(view, maxIndent)
- }
- previousMaxIndent = maxIndent
- },
- }
- return value
- },
- {
- decorations: value => value.decorations,
- }
- ),
- ]
- },
- }),
- ])
- }
- export const buildDecorations = (view: EditorView, maxIndent: number) => {
- const { state } = view
- const { doc, tabSize } = state
- const decorations: Range<Decoration>[] = []
- let from = 0
- for (const line of doc.iterLines()) {
- // const indent = line.match(/^(\s*)/)[1].length
- const indent = calculateIndent(line, tabSize, maxIndent)
- if (indent) {
- decorations.push(lineIndentDecoration(indent).range(from))
- }
- from += line.length + 1
- }
- return Decoration.set(decorations)
- }
- const lineIndentDecoration = (indent: number) =>
- Decoration.line({
- attributes: {
- // style: `text-indent: ${indent}ch hanging`, // "hanging" would be ideal, when browsers support it
- style: `text-indent: -${indent}ch; padding-left: calc(${indent}ch + 6px)`, // add 6px to account for existing padding-left
- },
- })
- // calculate the character width of whitespace at the start of a line
- const calculateIndent = (line: string, tabSize: number, maxIndent: number) => {
- let indent = 0
- for (const char of line) {
- if (char === ' ') {
- indent++
- } else if (char === '\t') {
- indent += tabSize - (indent % tabSize)
- } else {
- break
- }
- }
- return Math.min(indent, maxIndent)
- }
|