From 4a6b25f3a74e07daf2e73663a9a2f2ace7ef344a Mon Sep 17 00:00:00 2001 From: Muhammad Iqbal Fauzi Date: Sat, 22 Jul 2023 10:27:35 +0700 Subject: [PATCH] feat: add indentation on wrapped lines --- .../components/modes/textmode/TextMode.svelte | 5 +- .../codemirror/wrappedLineIndentation.ts | 110 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/modes/textmode/codemirror/wrappedLineIndentation.ts diff --git a/src/lib/components/modes/textmode/TextMode.svelte b/src/lib/components/modes/textmode/TextMode.svelte index c4947336..499d5f2b 100644 --- a/src/lib/components/modes/textmode/TextMode.svelte +++ b/src/lib/components/modes/textmode/TextMode.svelte @@ -73,6 +73,7 @@ import { needsFormatting } from '$lib/utils/jsonUtils.js' import { faJSONEditorFormat } from '$lib/img/customFontawesomeIcons.js' import { indentationMarkers } from '@replit/codemirror-indentation-markers' + import { wrappedLineIndentation } from './codemirror/wrappedLineIndentation.js' export let readOnly: boolean export let mainMenuBar: boolean @@ -516,7 +517,8 @@ tabSizeCompartment.of(EditorState.tabSize.of(tabSize)), indentUnitCompartment.of(createIndentUnit(indentation)), themeCompartment.of(EditorView.theme({}, { dark: hasDarkTheme() })), - EditorView.lineWrapping + EditorView.lineWrapping, + wrappedLineIndentation ] }) @@ -650,7 +652,6 @@ } const codeMirrorText = getCodeMirrorValue() - const isChanged = codeMirrorText !== text debug('onChangeCodeMirrorValue', { isChanged }) if (!isChanged) { diff --git a/src/lib/components/modes/textmode/codemirror/wrappedLineIndentation.ts b/src/lib/components/modes/textmode/codemirror/wrappedLineIndentation.ts new file mode 100644 index 00000000..263948bb --- /dev/null +++ b/src/lib/components/modes/textmode/codemirror/wrappedLineIndentation.ts @@ -0,0 +1,110 @@ +import { getIndentUnit } from '@codemirror/language' +import { EditorState, Facet, Line, RangeSetBuilder } from '@codemirror/state' +import { + Decoration, + EditorView, + ViewPlugin, + ViewUpdate, + type DecorationSet, + type PluginValue +} from '@codemirror/view' + +class WrappedLineIndentation implements PluginValue { + view: EditorView + decorations!: DecorationSet + initialPaddingLeft: string | null + indentUnit: number + + constructor(view: EditorView) { + this.view = view + this.indentUnit = getIndentUnit(view.state) + this.initialPaddingLeft = null + this.generate(view.state) + } + + update(update: ViewUpdate) { + const indentUnit = getIndentUnit(update.state) + const indentUnitChanged = indentUnit !== this.indentUnit + if (indentUnitChanged) { + this.indentUnit = indentUnit + } + if (update.docChanged || update.viewportChanged || indentUnitChanged) { + this.generate(update.state) + } + } + + private generate(state: EditorState) { + const builder = new RangeSetBuilder() + + // Measure the 'padding-left' value of the '.cm-line' class for the first time when DOM loads. + // As the DOM is not available during initial load, we use 'requestMeasure' to perform this task. + // After acquiring the initial 'padding-left' value, we can then generate decorations without further DOM measurements. + if (this.initialPaddingLeft) { + this.addStyleToBuilder(builder, state, this.initialPaddingLeft) + } else { + this.view.requestMeasure({ + read: (measure) => { + const lineElement = measure.contentDOM.querySelector('.cm-line') + + if (lineElement) { + this.initialPaddingLeft = window + .getComputedStyle(lineElement) + .getPropertyValue('padding-left') + this.addStyleToBuilder(builder, state, this.initialPaddingLeft) + } + this.decorations = builder.finish() + } + }) + } + + this.decorations = builder.finish() + } + + private addStyleToBuilder( + builder: RangeSetBuilder, + state: EditorState, + initialPaddingLeft: string + ) { + const visibleLines = this.getVisibleLines(state) + for (const line of visibleLines) { + const indentSize = this.getIndentSize(line) + const paddingValue = `calc(${indentSize + 2}ch + ${initialPaddingLeft})` + builder.add( + line.from, + line.from, + Decoration.line({ + attributes: { + style: `padding-left: ${paddingValue}; text-indent: -${indentSize + 2}ch;` + } + }) + ) + } + } + + // Get all lines that are currently visible in the viewport. + private getVisibleLines(state: EditorState) { + const lines = new Set() + for (const { from, to } of this.view.visibleRanges) { + let pos = from + while (pos <= to) { + const line = state.doc.lineAt(pos) + if (!lines.has(line)) { + lines.add(line) + } + pos = line.to + 1 + } + } + return lines + } + + private getIndentSize(line: Line) { + // is it possible to have tab character `\t`? + return line.text.length - line.text.trimStart().length + } +} + +export const wrappedLineIndentation = [ + ViewPlugin.fromClass(WrappedLineIndentation, { + decorations: (v) => v.decorations + }) +]