UNPKG

@sanity/code-input

Version:

Sanity input component for code, powered by CodeMirror

177 lines (159 loc) • 5.27 kB
/* eslint-disable no-param-reassign */ import {type Extension, StateEffect, StateField} from '@codemirror/state' import {Decoration, type DecorationSet, EditorView, lineNumbers} from '@codemirror/view' import type {ThemeContextValue} from '@sanity/ui' import {rgba} from '@sanity/ui/theme' import {getBackwardsCompatibleTone} from './backwardsCompatibleTone' const highlightLineClass = 'cm-highlight-line' export const addLineHighlight = StateEffect.define<number>() export const removeLineHighlight = StateEffect.define<number>() export const lineHighlightField = StateField.define({ create() { return Decoration.none }, update(lines, tr) { lines = lines.map(tr.changes) for (const e of tr.effects) { if (e.is(addLineHighlight)) { lines = lines.update({add: [lineHighlightMark.range(e.value)]}) } if (e.is(removeLineHighlight)) { lines = lines.update({ filter: (from) => { // removeLineHighlight value is lineStart for the highlight, so keep other effects return from !== e.value }, }) } } return lines }, toJSON(value, state) { const highlightLines: number[] = [] const iter = value.iter() while (iter.value) { const lineNumber = state.doc.lineAt(iter.from).number if (!highlightLines.includes(lineNumber)) { highlightLines.push(lineNumber) } iter.next() } return highlightLines }, fromJSON(value: number[], state) { const lines = state.doc.lines const highlights = value .filter((line) => line <= lines) // one-indexed .map((line) => lineHighlightMark.range(state.doc.line(line).from)) highlights.sort((a, b) => a.from - b.from) try { return Decoration.none.update({ add: highlights, }) } catch (e) { console.error(e) return Decoration.none } }, provide: (f) => EditorView.decorations.from(f), }) const lineHighlightMark = Decoration.line({ class: highlightLineClass, }) export const highlightState: { [prop: string]: StateField<DecorationSet> } = { highlight: lineHighlightField, } export interface HighlightLineConfig { onHighlightChange?: (lines: number[]) => void readOnly?: boolean theme: ThemeContextValue } function createCodeMirrorTheme(options: {themeCtx: ThemeContextValue}) { const {themeCtx} = options const fallbackTone = getBackwardsCompatibleTone(themeCtx) const dark = {color: themeCtx.theme.color.dark[fallbackTone]} const light = {color: themeCtx.theme.color.light[fallbackTone]} return EditorView.baseTheme({ '.cm-lineNumbers': { cursor: 'default', }, '.cm-line.cm-line': { position: 'relative', }, // need set background with pseudoelement so it does not render over selection color [`.${highlightLineClass}::before`]: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, zIndex: -3, content: "''", boxSizing: 'border-box', }, [`&dark .${highlightLineClass}::before`]: { background: rgba(dark.color.muted.caution.pressed.bg, 0.5), }, [`&light .${highlightLineClass}::before`]: { background: rgba(light.color.muted.caution.pressed.bg, 0.75), }, }) } export const highlightLine = (config: HighlightLineConfig): Extension => { const highlightTheme = createCodeMirrorTheme({themeCtx: config.theme}) return [ lineHighlightField, config.readOnly ? [] : lineNumbers({ domEventHandlers: { mousedown: (editorView, lineInfo) => { // Determine if the line for the clicked gutter line number has highlighted state or not const line = editorView.state.doc.lineAt(lineInfo.from) let isHighlighted = false editorView.state .field(lineHighlightField) .between(line.from, line.to, (from, to, value) => { if (value) { isHighlighted = true return false // stop iteration } return undefined }) if (isHighlighted) { editorView.dispatch({effects: removeLineHighlight.of(line.from)}) } else { editorView.dispatch({effects: addLineHighlight.of(line.from)}) } if (config?.onHighlightChange) { config.onHighlightChange(editorView.state.toJSON(highlightState).highlight) } return true }, }, }), highlightTheme, ] } /** * Adds and removes highlights to the provided view using highlightLines * @param view * @param highlightLines */ export function setHighlightedLines(view: EditorView, highlightLines: number[]): void { const doc = view.state.doc const lines = doc.lines //1-based line numbers const allLineNumbers = Array.from({length: lines}, (x, i) => i + 1) view.dispatch({ effects: allLineNumbers.map((lineNumber) => { const line = doc.line(lineNumber) if (highlightLines?.includes(lineNumber)) { return addLineHighlight.of(line.from) } return removeLineHighlight.of(line.from) }), }) }