strudel/repl/src/CodeMirror6.jsx
2022-04-25 22:00:18 +02:00

197 lines
5.5 KiB
JavaScript

import React from 'react';
import { CodeMirror as _CodeMirror } from 'react-codemirror6';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
// import { materialPalenight } from 'codemirror6-themes';
import { materialPalenight } from './themes/material-palenight';
const highlightMark = Decoration.mark({ class: 'cm-highlight' });
const addHighlight = StateEffect.define();
const removeHighlight = StateEffect.define();
const highlightTheme = EditorView.baseTheme({
'.cm-highlight': { outline: '1px solid #FFCA28' },
// '.cm-highlight': { background: '#FFCA28' },
});
const highlightField = StateField.define({
create() {
return Decoration.none;
},
update(highlights, tr) {
try {
highlights = highlights.map(tr.changes);
for (let e of tr.effects) {
if (e.is(addHighlight)) {
highlights = highlights.update({
add: [highlightMark.range(e.value.from, e.value.to)],
});
}
if (e.is(removeHighlight)) {
highlights = highlights.update({
filter: (f, t, value) => {
if (f === e.value.from && t === e.value.to) {
return false;
}
return true;
// console.log('filter', f,t,value, e.value.from, e.value.to);
},
});
}
}
return highlights;
} catch (err) {
// console.warn('highlighting error', err);
return highlights;
}
},
provide: (f) => EditorView.decorations.from(f),
});
// let timeouts = [];
export const highlightEvent = (event, view, code) => {
if (!view) {
return;
}
const ranges = event.context?.locations?.map(({ start, end }) => {
return [start, end].map(({ line, column }) => positionToOffset({ line: line - 1, ch: column }, code));
});
const effects = ranges.map(([from, to]) => addHighlight.of({ from, to }));
if (!effects.length) return false;
if (!view.state.field(highlightField, false)) {
effects.push(StateEffect.appendConfig.of([highlightField, highlightTheme]));
}
view.dispatch({ effects });
// const index = timeouts.length;
// timeouts = timeouts.filter(time)
/* const timeout = */ setTimeout(() => {
const effects = ranges.map(([from, to]) => removeHighlight.of({ from, to }));
view.dispatch({ effects });
// timeouts.splice(index, 1);
}, event.duration * 1000);
// timeouts.pusn({timeout,);
};
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
return (
<>
<_CodeMirror
onViewChange={onViewChanged}
style={{
display: 'flex',
flexDirection: 'column',
flex: '1 0 auto',
}}
value={value}
onChange={onChange}
extensions={[
javascript(),
materialPalenight,
// theme, language, ...
]}
/>
</>
);
}
let parenMark;
export const markParens = (editor, data) => {
const v = editor.getDoc().getValue();
const marked = getCurrentParenArea(v, data);
parenMark?.clear();
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
};
// returns { line, ch } from absolute character offset
export function offsetToPosition(offset, code) {
const lines = code.split('\n');
let line = 0;
let ch = 0;
for (let i = 0; i < offset; i++) {
if (ch === lines[line].length) {
line++;
ch = 0;
} else {
ch++;
}
}
return { line, ch };
}
// returns absolute character offset from { line, ch }
export function positionToOffset(position, code) {
const lines = code.split('\n');
if (position.line > lines.length) {
// throw new Error('positionToOffset: position.line > lines.length');
return 0;
}
let offset = 0;
for (let i = 0; i < position.line; i++) {
offset += lines[i].length + 1;
}
offset += position.ch;
return offset;
}
// given code and caret position, the functions returns the indices of the parens we are in
export function getCurrentParenArea(code, caretPosition) {
const caret = positionToOffset(caretPosition, code);
let open, i, begin, end;
// walk left
i = caret;
open = 0;
while (i > 0) {
if (code[i - 1] === '(') {
open--;
} else if (code[i - 1] === ')') {
open++;
}
if (open === -1) {
break;
}
i--;
}
begin = i;
// walk right
i = caret;
open = 0;
while (i < code.length) {
if (code[i] === '(') {
open--;
} else if (code[i] === ')') {
open++;
}
if (open === 1) {
break;
}
i++;
}
end = i;
return [begin, end].map((o) => offsetToPosition(o, code));
}
/*
export const markEvent = (editor) => (time, event) => {
const locs = event.context.locations;
if (!locs || !editor) {
return;
}
const col = event.context?.color || '#FFCA28';
// mark active event
const marks = locs.map(({ start, end }) =>
editor.getDoc().markText(
{ line: start.line - 1, ch: start.column },
{ line: end.line - 1, ch: end.column },
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
//{ css: `background-color: ${col};border-radius:5px` },
),
);
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
setTimeout(() => {
marks.forEach((mark) => mark.clear());
// }, '+' + event.duration * 0.5);
}, event.duration * 1000);
}; */