Skip to content

Commit

Permalink
feat: extensible code editor
Browse files Browse the repository at this point in the history
  • Loading branch information
stefan-gorules committed Jul 4, 2024
1 parent 9f56acc commit a387e65
Show file tree
Hide file tree
Showing 14 changed files with 97 additions and 17 deletions.
1 change: 1 addition & 0 deletions packages/jdm-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@ant-design/icons": "5.3.7",
"@codemirror/autocomplete": "^6.16.0",
"@codemirror/language": "^6.10.1",
"@codemirror/lint": "^6.8.1",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.26.3",
"@gorules/lezer-zen": "workspace:*",
Expand Down
22 changes: 20 additions & 2 deletions packages/jdm-editor/src/components/code-editor/ce.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Compartment, EditorState, Text } from '@codemirror/state';
import { Compartment, EditorState, type Extension, Text } from '@codemirror/state';
import { EditorView, placeholder as placeholderExt } from '@codemirror/view';
import { theme } from 'antd';
import clsx from 'clsx';
Expand All @@ -20,16 +20,21 @@ const updateListener = (onChange?: (data: string) => void, onStateChange?: (stat

const editorTheme = (isDark = false) => (isDark ? zenHighlightDark : zenHighlightLight);

type ExtensionParams = {
type?: 'standard' | 'unary' | 'template';
};

export type CodeEditorProps = {
maxRows?: number;
value?: string;
onChange?: (value: string) => void;
onStateChange?: (state: EditorState) => void;
placeholder?: string;
disabled?: boolean;
type?: 'standard' | 'template';
type?: 'unary' | 'standard' | 'template';
fullHeight?: boolean;
noStyle?: boolean;
extension?: (params: ExtensionParams) => Extension;
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'disabled' | 'onChange'>;

export const CodeEditor = React.forwardRef<HTMLDivElement, CodeEditorProps>(
Expand All @@ -45,6 +50,7 @@ export const CodeEditor = React.forwardRef<HTMLDivElement, CodeEditorProps>(
className,
onStateChange,
type = 'standard',
extension,
...props
},
ref,
Expand All @@ -60,6 +66,7 @@ export const CodeEditor = React.forwardRef<HTMLDivElement, CodeEditorProps>(
placeholder: new Compartment(),
readOnly: new Compartment(),
updateListener: new Compartment(),
userProvided: new Compartment(),
}),
[],
);
Expand All @@ -80,6 +87,7 @@ export const CodeEditor = React.forwardRef<HTMLDivElement, CodeEditorProps>(
compartment.theme.of(editorTheme(token.mode === 'dark')),
compartment.placeholder.of(placeholder ? placeholderExt(placeholder) : []),
compartment.readOnly.of(EditorView.editable.of(!disabled)),
compartment.userProvided.of(extension?.({ type }) ?? []),
],
}),
});
Expand Down Expand Up @@ -161,6 +169,16 @@ export const CodeEditor = React.forwardRef<HTMLDivElement, CodeEditorProps>(
});
}, [type]);

useEffect(() => {
if (!codeMirror.current) {
return;
}

codeMirror.current.dispatch({
effects: compartment.userProvided.reconfigure(extension?.({ type }) ?? []),
});
}, [extension, type]);

return (
<div
ref={composeRefs(container, ref)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,11 @@ const zenTemplateLanguage = new LanguageSupport(
);

type extensionOptions = {
type: 'standard' | 'template';
type: 'unary' | 'standard' | 'template';
};

export const zenExtensions = ({ type }: extensionOptions) => [
type === 'standard' ? zenLanguage : zenTemplateLanguage,
type !== 'template' ? zenLanguage : zenTemplateLanguage,
completionExtension(),
hoverExtension(),
closeBrackets(),
Expand Down
27 changes: 27 additions & 0 deletions packages/jdm-editor/src/components/code-editor/local-ce.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useEffect, useState } from 'react';

import { useDecisionGraphRaw } from '../decision-graph';
import { CodeEditor, type CodeEditorProps } from './ce';

type LocalCodeEditorProps = Omit<CodeEditorProps, 'extension'>;

export const LocalCodeEditor = React.forwardRef<HTMLDivElement, LocalCodeEditorProps>((props, ref) => {
const raw = useDecisionGraphRaw();
const [extension, setExtension] = useState<CodeEditorProps['extension']>(() => {
if (raw?.listenerStore) {
return raw.listenerStore.getState().onCodeExtension;
}

return undefined;
});

useEffect(() => {
if (!raw?.stateStore) {
return;
}

return raw.listenerStore.subscribe((s) => setExtension(() => s.onCodeExtension));
}, [raw]);

return <CodeEditor ref={ref} {...props} extension={extension} />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import type { EdgeChange, NodeChange, ReactFlowInstance, useEdgesState, useNodes
import type { StoreApi, UseBoundStore } from 'zustand';
import { create } from 'zustand';

import type { CodeEditorProps } from '../../code-editor';
import { mapToGraphEdge, mapToGraphEdges, mapToGraphNode, mapToGraphNodes } from '../dg-util';
import type { useGraphClipboard } from '../hooks/use-graph-clipboard';
import type { CustomNodeSpecification } from '../nodes/custom-node/index';
import type { CustomNodeSpecification } from '../nodes/custom-node';
import { NodeKind, type NodeSpecification } from '../nodes/specifications/specification-types';
import type { Simulation } from '../types/simulation.types';

Expand Down Expand Up @@ -107,6 +108,7 @@ export type DecisionGraphStoreType = {
onChange?: (val: DecisionGraphType) => void;
onPanelsChange?: (val?: string) => void;
onReactFlowInit?: (instance: ReactFlowInstance) => void;
onCodeExtension?: CodeEditorProps['extension'];
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type DecisionGraphEmptyType = {

onChange?: DecisionGraphStoreType['listeners']['onChange'];
onReactFlowInit?: DecisionGraphStoreType['listeners']['onReactFlowInit'];

onCodeExtension?: DecisionGraphStoreType['listeners']['onCodeExtension'];
};

export const DecisionGraphEmpty: React.FC<DecisionGraphEmptyType> = ({
Expand All @@ -46,6 +48,7 @@ export const DecisionGraphEmpty: React.FC<DecisionGraphEmptyType> = ({
simulate,
onPanelsChange,
onReactFlowInit,
onCodeExtension,
}) => {
const mountedRef = useRef(false);
const graphActions = useDecisionGraphActions();
Expand Down Expand Up @@ -79,8 +82,9 @@ export const DecisionGraphEmpty: React.FC<DecisionGraphEmptyType> = ({
listenerStore.setState({
onReactFlowInit,
onPanelsChange,
onCodeExtension,
});
}, [onReactFlowInit, onPanelsChange]);
}, [onReactFlowInit, onPanelsChange, onCodeExtension]);

useEffect(() => {
listenerStore.setState({ onChange: innerChange });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { useLayoutEffect, useState } from 'react';
import { Handle, Position } from 'reactflow';
import { P, match } from 'ts-pattern';

import { CodeEditor } from '../../../code-editor';
import { LocalCodeEditor } from '../../../code-editor/local-ce';
import { useDecisionGraphActions, useDecisionGraphState } from '../../context/dg-store.context';
import { GraphNode } from '../graph-node';
import type { MinimalNodeProps, NodeSpecification } from './specification-types';
Expand Down Expand Up @@ -178,7 +178,7 @@ const SwitchHandle: React.FC<{
return (
<div className={clsx('switchNode__statement')}>
<div className='switchNode__statement__inputArea'>
<CodeEditor
<LocalCodeEditor
placeholder={`Condition (e.g. x > 10)`}
style={{
fontSize: 12,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useEffect } from 'react';
import type { SchemaSelectProps } from '../../../helpers/components';
import { recursiveSelect } from '../../../helpers/components';
import { AutosizeTextArea } from '../../autosize-text-area';
import { CodeEditor } from '../../code-editor';
import { LocalCodeEditor } from '../../code-editor/local-ce';
import type { ColumnType, TableSchemaItem } from '../context/dt-store.context';

export type FieldAddProps = {
Expand Down Expand Up @@ -83,7 +83,7 @@ export const FieldAdd: React.FC<FieldAddProps> = (props) => {
label={type === 'expression' ? 'Selector' : 'Field'}
rules={[{ required: props.columnType === 'outputs' }]}
>
{props.columnType === 'inputs' ? <CodeEditor /> : <AutosizeTextArea maxRows={3} />}
{props.columnType === 'inputs' ? <LocalCodeEditor /> : <AutosizeTextArea maxRows={3} />}
</Form.Item>
<Form.Item name='defaultValue' label='Default Value'>
<Input autoComplete='off' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import type { SchemaSelectProps } from '../../../helpers/components';
import { getPath, recursiveSelect } from '../../../helpers/components';
import { AutosizeTextArea } from '../../autosize-text-area';
import { CodeEditor } from '../../code-editor';
import { LocalCodeEditor } from '../../code-editor/local-ce';
import type { ColumnType, TableSchemaItem } from '../context/dt-store.context';

export type FieldUpdateProps = {
Expand Down Expand Up @@ -91,7 +91,7 @@ export const FieldUpdate: React.FC<React.PropsWithChildren<FieldUpdateProps>> =
<Input />
</Form.Item>
<Form.Item name='field' label='Selector' rules={[{ required: props.columnType === 'outputs' }]}>
{props.columnType === 'inputs' ? <CodeEditor /> : <AutosizeTextArea maxRows={3} />}
{props.columnType === 'inputs' ? <LocalCodeEditor /> : <AutosizeTextArea maxRows={3} />}
</Form.Item>
<Form.Item name='defaultValue' label='Default Value'>
<Input autoComplete='off' />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { CellContext } from '@tanstack/react-table';
import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { match } from 'ts-pattern';

import { columnIdSelector } from '../../../helpers/components';
import { AutosizeTextArea } from '../../autosize-text-area';
import { CodeEditor } from '../../code-editor';
import { LocalCodeEditor } from '../../code-editor/local-ce';
import { type TableSchemaItem, useDecisionTableActions, useDecisionTableState } from '../context/dt-store.context';

export type TableDefaultCellProps = {
Expand Down Expand Up @@ -115,9 +116,12 @@ const TableInputCell: React.FC<TableCellProps> = ({ column, value, onChange, dis
}

return (
<CodeEditor
id={id}
<LocalCodeEditor
ref={textareaRef as any}
id={id}
type={match(column.colType)
.with('input', () => 'unary' as const)
.otherwise(() => 'standard' as const)}
className='grl-dt__cell__input'
maxRows={3}
value={value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import clsx from 'clsx';
import React, { useRef } from 'react';
import { useDrag, useDrop } from 'react-dnd';

import { CodeEditor } from '../code-editor';
import { LocalCodeEditor } from '../code-editor/local-ce';
import type { ExpressionEntry } from './context/expression-store.context';
import { useExpressionStore } from './context/expression-store.context';

Expand Down Expand Up @@ -80,7 +80,7 @@ export const ExpressionItem: React.FC<ExpressionItemProps> = ({ expression, inde
/>
</div>
<div className='expression-list-item__code'>
<CodeEditor
<LocalCodeEditor
placeholder='Expression'
maxRows={6}
disabled={disabled}
Expand Down
5 changes: 5 additions & 0 deletions packages/jdm-editor/src/helpers/codemirror.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { linter } from '@codemirror/lint';

export const codemirror = {
linter: linter,
};
2 changes: 2 additions & 0 deletions packages/jdm-editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ import './styles.scss';

export * from './components';
export * from './theme';

export { codemirror } from './helpers/codemirror';
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a387e65

Please sign in to comment.