From 400dd11c0e57d680a2b53ef0079e520a2fc96e5b Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 3 Jan 2024 19:28:03 +0100 Subject: [PATCH 01/12] feat: diff style --- .../components/CustomEditor/CustomEditor.tsx | 12 +- .../CustomEditor/PreviewExportEditor.tsx | 40 +++++- .../plugins/diff/DiffCheckbox.css.ts | 51 +++++++ .../plugins/diff/DiffCheckbox.tsx | 76 ++++++++++ .../plugins/diff/diffCheckboxEvents.ts | 11 ++ .../plugins/diff/diffCheckboxMarker.tsx | 135 ++++++++++++++++++ .../plugins/diff/diffMarkerStateIcon.css.ts | 15 ++ .../plugins/diff/diffMarkerStateIcon.tsx | 83 +++++++++++ .../CustomEditor/plugins/diff/diffTheme.ts | 27 ++++ .../src/components/Frame/Frame.css.ts | 6 + .../src/components/Frame/PreviewFrame.tsx | 9 +- .../PropertyEditor/EditorStyleForm.tsx | 24 +++- .../src/state/editor/config.store.ts | 12 ++ apps/codeimage/src/state/editor/editor.ts | 8 ++ apps/codeimage/src/state/editor/model.ts | 1 + packages/config/src/lib/base/languages.ts | 18 +++ .../src/lib/plugins/line-numbers-style.ts | 2 +- 17 files changed, 517 insertions(+), 13 deletions(-) create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/diff/DiffCheckbox.css.ts create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/diff/DiffCheckbox.tsx create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxEvents.ts create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.css.ts create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/diff/diffTheme.ts diff --git a/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx b/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx index f829423dd..e11ce73f5 100644 --- a/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx +++ b/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx @@ -35,6 +35,7 @@ import { VoidProps, } from 'solid-js'; import {createTabIcon} from '../../hooks/use-tab-icon'; +import {diffMarkerExtension} from './plugins/diff/diffCheckboxMarker'; const EDITOR_BASE_SETUP: Extension = [ highlightSpecialChars(), @@ -172,9 +173,14 @@ export default function CustomEditor(props: VoidProps) { createExtension(() => customFontExtension()); createExtension(currentLanguage); createExtension(currentExtraLanguage); - createExtension(() => - editorState.options.showLineNumbers ? lineNumbers() : [], - ); + createExtension(() => { + return [ + editorState.options.showDiffMode + ? diffMarkerExtension({readOnly: props.readOnly}) + : [], + editorState.options.showLineNumbers ? lineNumbers() : [], + ]; + }); createExtension(() => themeConfiguration()?.editorTheme || []); createExtension(baseTheme); diff --git a/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx b/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx index fdcd457eb..1208b137f 100644 --- a/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx +++ b/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx @@ -1,16 +1,39 @@ import {getRootEditorStore} from '@codeimage/store/editor'; -import {Annotation, Transaction} from '@codemirror/state'; +import {Annotation, StateEffect, Transaction} from '@codemirror/state'; import {EditorView} from '@codemirror/view'; +import {createCompartmentExtension} from 'solid-codemirror'; import {createEffect, createSignal, lazy, on} from 'solid-js'; +import {diffCheckboxEffect} from './plugins/diff/diffCheckboxMarker'; +import {diffMarkerStateIconGutterExtension} from './plugins/diff/diffMarkerStateIcon'; const syncAnnotation = Annotation.define(); function syncDispatch(tr: Transaction, other: EditorView) { - if (!tr.changes.empty && !tr.annotation(syncAnnotation)) { - const annotations: Annotation[] = [syncAnnotation.of(true)]; + if (tr.annotation(syncAnnotation)) { + return; + } + const annotations: Annotation[] = [syncAnnotation.of(true)]; + const effects: StateEffect[] = []; + let changed = false; + if (!tr.changes.empty) { + changed = true; const userEvent = tr.annotation(Transaction.userEvent); if (userEvent) annotations.push(Transaction.userEvent.of(userEvent)); - other.dispatch({changes: tr.changes, annotations}); + } + const stateEffects = tr.effects + .filter(effect => !!effect) + // TODO: add configuration + .filter(effect => effect.is(diffCheckboxEffect)); + if (stateEffects.length) { + changed = true; + effects.push(...stateEffects); + } + if (changed) { + other.dispatch({ + changes: tr.changes, + effects, + annotations, + }); } } @@ -27,12 +50,17 @@ export default function PreviewExportEditor(props: PreviewExportEditorProps) { on(editorView, editorView => { if (!editorView) return; getRootEditorStore().canvasEditorEvents.listen(tr => { - setTimeout(() => syncDispatch(tr, editorView), 250); - setInterval(() => editorView.requestMeasure()); + setTimeout(() => syncDispatch(tr, editorView), 100); + setTimeout(() => editorView.requestMeasure()); }); }), ); + createCompartmentExtension( + () => diffMarkerStateIconGutterExtension, + editorView, + ); + return ( { diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/DiffCheckbox.css.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/DiffCheckbox.css.ts new file mode 100644 index 000000000..a1553fab4 --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/DiffCheckbox.css.ts @@ -0,0 +1,51 @@ +import {themeTokens, themeVars} from '@codeui/kit'; +import {createVar, style} from '@vanilla-extract/css'; +import {recipe} from '@vanilla-extract/recipes'; + +export const container = style({ + opacity: 1, + transition: 'opacity 150ms ease-in-out', + display: 'flex', + height: '100%', +}); + +export const tooltip = style({ + fontSize: themeTokens.fontSize.xs, +}); + +const iconBorder = createVar(); +export const icon = recipe({ + base: { + width: '20px', + height: '20px', + outline: 'none', + border: `1px solid ${iconBorder}`, + vars: { + [iconBorder]: '#cccccc20', + }, + ':active': { + transform: 'scale(1) !important', + }, + ':focus-visible': { + outline: `none`, + borderColor: themeVars.brand, + }, + selectors: { + '[data-theme-mode=light] &': { + background: 'rgba(0,0,0,0.05)', + color: 'black', + }, + '[data-theme-mode=light] &:hover': { + background: 'rgba(0,0,0,0.1)', + color: 'black', + }, + '[data-theme-mode=dark] &': { + background: 'rgba(0,0,0,0.3)', + color: 'white', + }, + '[data-theme-mode=dark] &:hover': { + background: 'rgba(0,0,0,0.5)', + }, + }, + }, +}); diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/DiffCheckbox.tsx b/apps/codeimage/src/components/CustomEditor/plugins/diff/DiffCheckbox.tsx new file mode 100644 index 000000000..1c81bdd6d --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/DiffCheckbox.tsx @@ -0,0 +1,76 @@ +import {IconButton, Tooltip} from '@codeui/kit'; +import {createSignal} from 'solid-js'; +import {icon, tooltip} from './DiffCheckbox.css'; + +export type DiffCheckboxState = 'added' | 'removed' | 'untouched'; + +interface DiffCheckboxProps { + value: DiffCheckboxState; + onChange: (value: DiffCheckboxState) => void; +} + +export function DiffCheckbox(props: DiffCheckboxProps) { + let el!: HTMLButtonElement; + const [tooltipOpen, setTooltipOpen] = createSignal(false); + const statesTransition: Record = { + added: 'removed', + removed: 'untouched', + untouched: 'added', + }; + + const onClick = (value: DiffCheckboxState) => { + props.onChange(statesTransition[value]); + if (el.matches(':hover').valueOf()) { + requestAnimationFrame(() => setTooltipOpen(true)); + } + }; + + const label = () => { + switch (props.value) { + case 'added': + return '+'; + case 'removed': + return '-'; + case 'untouched': + return null; + } + }; + + const title = () => { + switch (props.value) { + case 'added': + return 'Set to removed'; + case 'removed': + return 'Set to untouched'; + case 'untouched': + return 'Set to added'; + } + }; + + return ( +
+ + onClick(props.value)} + > + {label()} + + +
+ ); +} diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxEvents.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxEvents.ts new file mode 100644 index 000000000..7bfd3855c --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxEvents.ts @@ -0,0 +1,11 @@ +import {Line} from '@codemirror/state'; +import {createEventBus} from '@solid-primitives/event-bus'; +import {createRoot} from 'solid-js'; +import {DiffCheckboxState} from './DiffCheckbox'; + +export const diffCheckboxEvents = createRoot(() => + createEventBus<{ + state: DiffCheckboxState | null; + line: Line; + }>(), +); diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx new file mode 100644 index 000000000..6bfd28af5 --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx @@ -0,0 +1,135 @@ +import {StateEffect, StateField} from '@codemirror/state'; +import {Decoration, EditorView, gutter, GutterMarker} from '@codemirror/view'; +import {createRoot, createSignal, onCleanup} from 'solid-js'; +import {diffCheckboxEvents} from './diffCheckboxEvents'; +import {diffTheme} from './diffTheme'; +import {DiffCheckbox, DiffCheckboxState} from './DiffCheckbox'; +import {container} from './DiffCheckbox.css'; + +export const diffCheckboxEffect = StateEffect.define<{ + pos: number; + state: DiffCheckboxState | null; +}>({ + map: (val, mapping) => ({ + pos: mapping.mapPos(val.pos), + state: val.state, + }), +}); + +const diffLineState = StateField.define({ + create() { + return Decoration.none; + }, + update(set, transaction) { + set = set.map(transaction.changes); + for (const effect of transaction.effects) { + if (effect.is(diffCheckboxEffect)) { + const line = transaction.state.doc.lineAt(effect.value.pos); + const decoration = + effect.value.state === 'added' + ? addDecoration + : effect.value.state === 'removed' + ? removeDecoration + : null; + if (!decoration) { + set = set.update({filter: from => from !== line.from}); + } else { + set = set.update({ + add: [decoration.range(line.from, line.from)], + filter: from => from !== line.from, + }); + } + diffCheckboxEvents.emit({ + state: effect.value.state, + line: line, + }); + } + } + return set; + }, + provide: field => EditorView.decorations.from(field), +}); + +const addDecoration = Decoration.line({ + attributes: {class: 'cm-add-line'}, +}); + +const removeDecoration = Decoration.line({ + attributes: {class: 'cm-remove-line'}, +}); + +function setDiffState( + view: EditorView, + lineNumber: number, + state: DiffCheckboxState, +) { + const line = view.state.doc.line(lineNumber); + view.dispatch({ + effects: diffCheckboxEffect.of({pos: line.from, state: state}), + }); +} + +class DiffCheckboxMarker extends GutterMarker { + private dispose: VoidFunction | null = null; + + constructor(private readonly lineNumber: number) { + super(); + } + + eq(other: DiffCheckboxMarker) { + return this.lineNumber == other.lineNumber; + } + + destroy(dom: Node) { + super.destroy(dom); + this.dispose?.(); + } + + toDOM(view: EditorView): Node { + return createRoot(dispose => { + // eslint-disable-next-line solid/reactivity + this.dispose = dispose; + const [value, setValue] = createSignal('untouched'); + + const unsubscribe = diffCheckboxEvents.listen(({state, line}) => { + if (line.number === this.lineNumber) { + setValue(state ?? 'untouched'); + } + }); + + onCleanup(() => unsubscribe()); + + return ( +
+ setDiffState(view, this.lineNumber, state)} + /> +
+ ); + }) as Node; + } +} + +interface DiffMarkerExtensionOptions { + readOnly: boolean; +} + +export const diffMarkerExtension = (options: DiffMarkerExtensionOptions) => { + if (options.readOnly) { + return [diffLineState, diffTheme]; + } + return [ + diffLineState, + diffTheme, + gutter({ + class: 'cm-checkboxMarkerExtension', + renderEmptyElements: false, + lineMarker(view, line) { + return new DiffCheckboxMarker(view.state.doc.lineAt(line.from).number); + }, + lineMarkerChange: () => false, + widgetMarker: () => null, + }), + ]; +}; diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.css.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.css.ts new file mode 100644 index 000000000..c989e0b68 --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.css.ts @@ -0,0 +1,15 @@ +import {backgroundColorVar} from '@codeimage/ui'; +import {style} from '@vanilla-extract/css'; + +export const wrapper = style({ + height: '100%', +}); + +export const icon = style({ + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + paddingLeft: '4px', + backgroundColor: backgroundColorVar, +}); diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx new file mode 100644 index 000000000..1110a3be5 --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx @@ -0,0 +1,83 @@ +import {backgroundColorVar} from '@codeimage/ui'; +import {gutter, GutterMarker} from '@codemirror/view'; +import {assignInlineVars} from '@vanilla-extract/dynamic'; +import {createRoot, createSignal, onCleanup, Show} from 'solid-js'; +import {diffCheckboxEvents} from './diffCheckboxEvents'; +import {colors} from './diffTheme'; +import {DiffCheckboxState} from './DiffCheckbox'; +import * as styles from './diffMarkerStateIcon.css'; + +interface MarkerStateSymbolOption { + label: string; + color: string; +} + +class MarkerStateIcon extends GutterMarker { + private dispose: VoidFunction | null = null; + + symbols: Record = { + added: {label: '+', color: colors.addLine}, + removed: {label: '-', color: colors.removeLine}, + untouched: null, + }; + + constructor(public readonly lineNumber: number) { + super(); + } + + destroy(dom: Node) { + super.destroy(dom); + this.dispose?.(); + } + + eq(other: MarkerStateIcon): boolean { + return other.lineNumber === this.lineNumber; + } + + toDOM() { + return createRoot(dispose => { + // eslint-disable-next-line solid/reactivity + this.dispose = dispose; + // TODO: add initial state + const [state, setState] = createSignal('untouched'); + const currentSymbol = () => this.symbols[state()]; + + const unsubscribe = diffCheckboxEvents.listen(({state, line}) => { + if (line.number === this.lineNumber) { + setState(state ?? 'untouched'); + } + }); + + onCleanup(() => unsubscribe()); + + return ( +
+ } when={currentSymbol()}> + {currentSymbol => ( +
+ {currentSymbol()?.label} +
+ )} +
+
+ ) as Node; + }); + } +} + +export const diffMarkerStateIconGutterExtension = [ + gutter({ + class: 'cm-checkboxMarkerStateIconGutter', + renderEmptyElements: true, + lineMarker(view, line) { + return new MarkerStateIcon(view.state.doc.lineAt(line.from).number); + }, + lineMarkerChange: () => false, + widgetMarker: () => null, + }), +]; diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffTheme.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffTheme.ts new file mode 100644 index 000000000..71bc05bef --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffTheme.ts @@ -0,0 +1,27 @@ +import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'; +import {EditorView} from '@codemirror/view'; +import {tags} from '@lezer/highlight'; + +export const colors = { + removeLine: 'hsla(339, 100%, 57%, 25%)', + removeToken: 'hsla(339, 100%, 57%, 100%)', + addLine: 'hsl(120, 81%, 38%, 25%)', + addToken: 'hsl(120, 81%, 38%, 100%)', +}; + +export const diffTheme = [ + EditorView.theme({ + '.cm-remove-line': { + backgroundColor: colors.removeLine, + }, + '.cm-add-line': { + backgroundColor: colors.addLine, + }, + }), + syntaxHighlighting( + HighlightStyle.define([ + {tag: tags.inserted, color: colors.addToken}, + {tag: tags.deleted, color: colors.removeToken}, + ]), + ), +]; diff --git a/apps/codeimage/src/components/Frame/Frame.css.ts b/apps/codeimage/src/components/Frame/Frame.css.ts index 1f0668208..5c583988a 100644 --- a/apps/codeimage/src/components/Frame/Frame.css.ts +++ b/apps/codeimage/src/components/Frame/Frame.css.ts @@ -63,6 +63,12 @@ export const previewPortal = style({ height: 'auto', opacity: 0, transformOrigin: 'left top', + selectors: { + '&[data-dev-mode]': { + opacity: 1, + zIndex: 999, + }, + }, }); export const container = style([ diff --git a/apps/codeimage/src/components/Frame/PreviewFrame.tsx b/apps/codeimage/src/components/Frame/PreviewFrame.tsx index 2bdc66e66..c100ba361 100644 --- a/apps/codeimage/src/components/Frame/PreviewFrame.tsx +++ b/apps/codeimage/src/components/Frame/PreviewFrame.tsx @@ -2,6 +2,7 @@ import {getAssetsStore, isAssetUrl} from '@codeimage/store/assets/assets'; import {AssetsImage} from '@codeimage/store/assets/AssetsImage'; import {getRootEditorStore} from '@codeimage/store/editor'; import {getActiveEditorStore} from '@codeimage/store/editor/activeEditor'; +import {EditorConfigStore} from '@codeimage/store/editor/config.store'; import {getFrameState} from '@codeimage/store/editor/frame'; import {getTerminalState} from '@codeimage/store/editor/terminal'; import {dispatchCopyToClipboard} from '@codeimage/store/effects/onCopyToClipboard'; @@ -17,6 +18,7 @@ import { VoidProps, } from 'solid-js'; import {Portal} from 'solid-js/web'; +import {provideState} from 'statebuilder'; import {setPreviewEditorView} from '../../hooks/export-snippet'; import {useHotkey} from '../../hooks/use-hotkey'; import {DynamicTerminal} from '../Terminal/DynamicTerminal/DynamicTerminal'; @@ -32,9 +34,14 @@ const PreviewExportEditor = lazy( ); function PreviewPortal(props: ParentProps) { + const config = provideState(EditorConfigStore); return ( -
+
}>{props.children}
diff --git a/apps/codeimage/src/components/PropertyEditor/EditorStyleForm.tsx b/apps/codeimage/src/components/PropertyEditor/EditorStyleForm.tsx index 065acead1..6832aa779 100644 --- a/apps/codeimage/src/components/PropertyEditor/EditorStyleForm.tsx +++ b/apps/codeimage/src/components/PropertyEditor/EditorStyleForm.tsx @@ -6,7 +6,7 @@ import {getRootEditorStore} from '@codeimage/store/editor'; import {getActiveEditorStore} from '@codeimage/store/editor/activeEditor'; import {dispatchUpdateTheme} from '@codeimage/store/effects/onThemeChange'; import {getThemeStore} from '@codeimage/store/theme/theme.store'; -import {createSelectOptions, Select} from '@codeui/kit'; +import {Checkbox, createSelectOptions, Select} from '@codeui/kit'; import {getUmami} from '@core/constants/umami'; import {DynamicSizedContainer} from '@ui/DynamicSizedContainer/DynamicSizedContainer'; import {SegmentedField} from '@ui/SegmentedField/SegmentedField'; @@ -40,7 +40,13 @@ export const EditorStyleForm: ParentComponent = () => { getActiveEditorStore(); const { state, - actions: {setShowLineNumbers, setFontWeight, setFontId, setEnableLigatures}, + actions: { + setShowLineNumbers, + setFontWeight, + setFontId, + setEnableLigatures, + setEnableDiff, + }, computed: {selectedFont}, } = getRootEditorStore(); @@ -205,6 +211,20 @@ export const EditorStyleForm: ParentComponent = () => { + + + + } + > + + + + diff --git a/apps/codeimage/src/state/editor/config.store.ts b/apps/codeimage/src/state/editor/config.store.ts index eb1800484..8fe474775 100644 --- a/apps/codeimage/src/state/editor/config.store.ts +++ b/apps/codeimage/src/state/editor/config.store.ts @@ -21,6 +21,13 @@ export interface ConfigState { ready: boolean; fonts: (CustomFontConfiguration & {type: 'web'})[]; systemFonts: (CustomFontConfiguration & {type: 'system'})[]; + devMode: boolean; +} + +declare global { + interface Window { + toggleDevMode: () => void; + } } function getDefaultConfig(): ConfigState { @@ -28,6 +35,7 @@ function getDefaultConfig(): ConfigState { ready: false, fonts: [...SUPPORTED_FONTS], systemFonts: [], + devMode: false, }; } @@ -57,6 +65,10 @@ export const EditorConfigStore = defineStore(() => getDefaultConfig()) .extend(_ => { const fonts = createMemo(() => _.localFontsApi.state().fonts); + onMount(() => { + window.toggleDevMode = () => _.set('devMode', debug => !debug); + }); + const buildSystemFontConfiguration = (font: LoadedFont) => ({ type: 'system', diff --git a/apps/codeimage/src/state/editor/editor.ts b/apps/codeimage/src/state/editor/editor.ts index 987087c8a..41b1db20d 100644 --- a/apps/codeimage/src/state/editor/editor.ts +++ b/apps/codeimage/src/state/editor/editor.ts @@ -37,6 +37,7 @@ export function getInitialEditorUiOptions(): EditorUIOptions { fontWeight: appEnvironment.defaultState.editor.font.types[0].weight, focused: false, enableLigatures: true, + showDiff: true, }; } @@ -55,9 +56,11 @@ export function createEditorsStore() { setThemeId: string; setFontWeight: number; setShowLineNumbers: boolean; + setLineNumbersStart: number; setFromPersistedState: PersistedEditorState; setFromPreset: PresetData['editor']; setEnableLigatures: boolean; + setShowDiff: boolean; }>(), ); @@ -92,6 +95,9 @@ export function createEditorsStore() { .hold(store.commands.setEnableLigatures, (enable, {set}) => set('options', 'enableLigatures', enable), ) + .hold(store.commands.setShowDiff, (enable, {set}) => + set('options', 'showDiffMode', enable), + ) .hold(store.commands.setFromPreset, presetData => { store.set('options', presetData); store.dispatch(editorUpdateCommand, void 0); @@ -144,6 +150,7 @@ export function createEditorsStore() { fontId: state.options.fontId, fontWeight: state.options.fontWeight, enableLigatures: state.options.enableLigatures ?? true, + showDiffMode: state.options.showDiffMode ?? false, }, }; }; @@ -155,6 +162,7 @@ export function createEditorsStore() { store.commands.setFontWeight, store.commands.setShowLineNumbers, store.commands.setEnableLigatures, + store.commands.setShowDiff, editorUpdateCommand, ]), ).pipe( diff --git a/apps/codeimage/src/state/editor/model.ts b/apps/codeimage/src/state/editor/model.ts index dbb2a4cf9..eb649140b 100644 --- a/apps/codeimage/src/state/editor/model.ts +++ b/apps/codeimage/src/state/editor/model.ts @@ -6,6 +6,7 @@ export interface EditorUIOptions { showLineNumbers: boolean; focused: boolean; themeId: string; + showDiffMode: boolean; } export interface TabState { diff --git a/packages/config/src/lib/base/languages.ts b/packages/config/src/lib/base/languages.ts index d58da0056..7416703ea 100644 --- a/packages/config/src/lib/base/languages.ts +++ b/packages/config/src/lib/base/languages.ts @@ -660,4 +660,22 @@ export const SUPPORTED_LANGUAGES: readonly LanguageDefinition[] = [ }, ], }, + { + id: 'git-patch', + label: 'Patch', + color: '#fff', + plugin: () => + Promise.all([ + importLegacy(), + import('@codemirror/legacy-modes/mode/diff'), + ]).then(([cb, m]) => cb(m.diff)), + icons: [ + { + name: 'Git', + extension: '.patch', + content: () => import('material-icon-theme/icons/diff.svg?raw'), + matcher: /^.*\.(patch)$/, + }, + ], + }, ]; diff --git a/packages/highlight/src/lib/plugins/line-numbers-style.ts b/packages/highlight/src/lib/plugins/line-numbers-style.ts index a0a12f84d..8ca503a40 100644 --- a/packages/highlight/src/lib/plugins/line-numbers-style.ts +++ b/packages/highlight/src/lib/plugins/line-numbers-style.ts @@ -6,6 +6,6 @@ export interface StyledLineNumbersOptions { export function styledLineNumbers(options: StyledLineNumbersOptions) { return EditorView.theme({ - '.cm-lineNumbers .cm-gutterElement': {color: options.color}, + '.cm-gutter .cm-gutterElement': {color: options.color}, }); } From 2fcd7063a81beb24a23be84efe094bf5525db7de Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 3 Jan 2024 19:48:11 +0100 Subject: [PATCH 02/12] feat: diff style --- .../components/CustomEditor/CustomEditor.tsx | 4 +- .../CustomEditor/PreviewExportEditor.tsx | 47 ++------ .../plugins/diff/diffCheckboxMarker.tsx | 101 ++---------------- .../plugins/diff/diffMarkerStateIcon.tsx | 22 +--- .../diff/{diffCheckboxEvents.ts => events.ts} | 2 +- .../CustomEditor/plugins/diff/extension.ts | 42 ++++++++ .../CustomEditor/plugins/diff/state.ts | 90 ++++++++++++++++ .../plugins/diff/{diffTheme.ts => theme.ts} | 9 +- .../CustomEditor/plugins/sync/sync.ts | 37 +++++++ .../CustomEditor/registered-effects.ts | 3 + .../PropertyEditor/EditorStyleForm.tsx | 6 +- packages/highlight/README.md | 2 +- packages/highlight/rollup.config.js | 2 +- packages/vanilla-extract/rollup.config.js | 4 +- 14 files changed, 208 insertions(+), 163 deletions(-) rename apps/codeimage/src/components/CustomEditor/plugins/diff/{diffCheckboxEvents.ts => events.ts} (84%) create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts rename apps/codeimage/src/components/CustomEditor/plugins/diff/{diffTheme.ts => theme.ts} (62%) create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/sync/sync.ts create mode 100644 apps/codeimage/src/components/CustomEditor/registered-effects.ts diff --git a/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx b/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx index e11ce73f5..885e8dbf3 100644 --- a/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx +++ b/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx @@ -35,7 +35,7 @@ import { VoidProps, } from 'solid-js'; import {createTabIcon} from '../../hooks/use-tab-icon'; -import {diffMarkerExtension} from './plugins/diff/diffCheckboxMarker'; +import {diffMarkerControl} from './plugins/diff/extension'; const EDITOR_BASE_SETUP: Extension = [ highlightSpecialChars(), @@ -176,7 +176,7 @@ export default function CustomEditor(props: VoidProps) { createExtension(() => { return [ editorState.options.showDiffMode - ? diffMarkerExtension({readOnly: props.readOnly}) + ? diffMarkerControl({readOnly: props.readOnly}) : [], editorState.options.showLineNumbers ? lineNumbers() : [], ]; diff --git a/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx b/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx index 1208b137f..97a8bf3f4 100644 --- a/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx +++ b/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx @@ -1,41 +1,9 @@ import {getRootEditorStore} from '@codeimage/store/editor'; -import {Annotation, StateEffect, Transaction} from '@codemirror/state'; import {EditorView} from '@codemirror/view'; import {createCompartmentExtension} from 'solid-codemirror'; import {createEffect, createSignal, lazy, on} from 'solid-js'; -import {diffCheckboxEffect} from './plugins/diff/diffCheckboxMarker'; -import {diffMarkerStateIconGutterExtension} from './plugins/diff/diffMarkerStateIcon'; - -const syncAnnotation = Annotation.define(); - -function syncDispatch(tr: Transaction, other: EditorView) { - if (tr.annotation(syncAnnotation)) { - return; - } - const annotations: Annotation[] = [syncAnnotation.of(true)]; - const effects: StateEffect[] = []; - let changed = false; - if (!tr.changes.empty) { - changed = true; - const userEvent = tr.annotation(Transaction.userEvent); - if (userEvent) annotations.push(Transaction.userEvent.of(userEvent)); - } - const stateEffects = tr.effects - .filter(effect => !!effect) - // TODO: add configuration - .filter(effect => effect.is(diffCheckboxEffect)); - if (stateEffects.length) { - changed = true; - effects.push(...stateEffects); - } - if (changed) { - other.dispatch({ - changes: tr.changes, - effects, - annotations, - }); - } -} +import {diffMarkerStateIconGutter} from './plugins/diff/extension'; +import {syncDispatch} from './plugins/sync/sync'; const CustomEditor = lazy(() => import('./CustomEditor')); @@ -50,16 +18,15 @@ export default function PreviewExportEditor(props: PreviewExportEditorProps) { on(editorView, editorView => { if (!editorView) return; getRootEditorStore().canvasEditorEvents.listen(tr => { - setTimeout(() => syncDispatch(tr, editorView), 100); - setTimeout(() => editorView.requestMeasure()); + setTimeout(() => { + syncDispatch(tr, editorView); + editorView.requestMeasure(); + }, 100); }); }), ); - createCompartmentExtension( - () => diffMarkerStateIconGutterExtension, - editorView, - ); + createCompartmentExtension(diffMarkerStateIconGutter, editorView); return ( ({ - map: (val, mapping) => ({ - pos: mapping.mapPos(val.pos), - state: val.state, - }), -}); - -const diffLineState = StateField.define({ - create() { - return Decoration.none; - }, - update(set, transaction) { - set = set.map(transaction.changes); - for (const effect of transaction.effects) { - if (effect.is(diffCheckboxEffect)) { - const line = transaction.state.doc.lineAt(effect.value.pos); - const decoration = - effect.value.state === 'added' - ? addDecoration - : effect.value.state === 'removed' - ? removeDecoration - : null; - if (!decoration) { - set = set.update({filter: from => from !== line.from}); - } else { - set = set.update({ - add: [decoration.range(line.from, line.from)], - filter: from => from !== line.from, - }); - } - diffCheckboxEvents.emit({ - state: effect.value.state, - line: line, - }); - } - } - return set; - }, - provide: field => EditorView.decorations.from(field), -}); - -const addDecoration = Decoration.line({ - attributes: {class: 'cm-add-line'}, -}); - -const removeDecoration = Decoration.line({ - attributes: {class: 'cm-remove-line'}, -}); - -function setDiffState( - view: EditorView, - lineNumber: number, - state: DiffCheckboxState, -) { - const line = view.state.doc.line(lineNumber); - view.dispatch({ - effects: diffCheckboxEffect.of({pos: line.from, state: state}), - }); -} - -class DiffCheckboxMarker extends GutterMarker { +export class DiffCheckboxMarker extends GutterMarker { private dispose: VoidFunction | null = null; constructor(private readonly lineNumber: number) { @@ -91,7 +27,7 @@ class DiffCheckboxMarker extends GutterMarker { this.dispose = dispose; const [value, setValue] = createSignal('untouched'); - const unsubscribe = diffCheckboxEvents.listen(({state, line}) => { + const unsubscribe = events.listen(({state, line}) => { if (line.number === this.lineNumber) { setValue(state ?? 'untouched'); } @@ -103,33 +39,12 @@ class DiffCheckboxMarker extends GutterMarker {
setDiffState(view, this.lineNumber, state)} + onChange={state => + dispatchUpdateDiffLineState(view, this.lineNumber, state) + } />
); }) as Node; } } - -interface DiffMarkerExtensionOptions { - readOnly: boolean; -} - -export const diffMarkerExtension = (options: DiffMarkerExtensionOptions) => { - if (options.readOnly) { - return [diffLineState, diffTheme]; - } - return [ - diffLineState, - diffTheme, - gutter({ - class: 'cm-checkboxMarkerExtension', - renderEmptyElements: false, - lineMarker(view, line) { - return new DiffCheckboxMarker(view.state.doc.lineAt(line.from).number); - }, - lineMarkerChange: () => false, - widgetMarker: () => null, - }), - ]; -}; diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx index 1110a3be5..8e0aad85f 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx @@ -2,8 +2,8 @@ import {backgroundColorVar} from '@codeimage/ui'; import {gutter, GutterMarker} from '@codemirror/view'; import {assignInlineVars} from '@vanilla-extract/dynamic'; import {createRoot, createSignal, onCleanup, Show} from 'solid-js'; -import {diffCheckboxEvents} from './diffCheckboxEvents'; -import {colors} from './diffTheme'; +import {events} from './events'; +import {colors} from './theme'; import {DiffCheckboxState} from './DiffCheckbox'; import * as styles from './diffMarkerStateIcon.css'; @@ -12,7 +12,7 @@ interface MarkerStateSymbolOption { color: string; } -class MarkerStateIcon extends GutterMarker { +export class DiffGutterMarkerStateIcon extends GutterMarker { private dispose: VoidFunction | null = null; symbols: Record = { @@ -30,7 +30,7 @@ class MarkerStateIcon extends GutterMarker { this.dispose?.(); } - eq(other: MarkerStateIcon): boolean { + eq(other: DiffGutterMarkerStateIcon): boolean { return other.lineNumber === this.lineNumber; } @@ -42,7 +42,7 @@ class MarkerStateIcon extends GutterMarker { const [state, setState] = createSignal('untouched'); const currentSymbol = () => this.symbols[state()]; - const unsubscribe = diffCheckboxEvents.listen(({state, line}) => { + const unsubscribe = events.listen(({state, line}) => { if (line.number === this.lineNumber) { setState(state ?? 'untouched'); } @@ -69,15 +69,3 @@ class MarkerStateIcon extends GutterMarker { }); } } - -export const diffMarkerStateIconGutterExtension = [ - gutter({ - class: 'cm-checkboxMarkerStateIconGutter', - renderEmptyElements: true, - lineMarker(view, line) { - return new MarkerStateIcon(view.state.doc.lineAt(line.from).number); - }, - lineMarkerChange: () => false, - widgetMarker: () => null, - }), -]; diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxEvents.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/events.ts similarity index 84% rename from apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxEvents.ts rename to apps/codeimage/src/components/CustomEditor/plugins/diff/events.ts index 7bfd3855c..f96ec55d5 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxEvents.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/events.ts @@ -3,7 +3,7 @@ import {createEventBus} from '@solid-primitives/event-bus'; import {createRoot} from 'solid-js'; import {DiffCheckboxState} from './DiffCheckbox'; -export const diffCheckboxEvents = createRoot(() => +export const events = createRoot(() => createEventBus<{ state: DiffCheckboxState | null; line: Line; diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts new file mode 100644 index 000000000..051c35438 --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts @@ -0,0 +1,42 @@ +import {gutter} from '@codemirror/view'; +import {DiffCheckboxMarker} from './diffCheckboxMarker'; +import {diffLineState} from './state'; +import {theme} from './theme'; +import {DiffGutterMarkerStateIcon} from './diffMarkerStateIcon'; + +interface DiffMarkerExtensionOptions { + readOnly: boolean; +} + +export const diffMarkerControl = (options: DiffMarkerExtensionOptions) => { + const base = [diffLineState, theme]; + if (options.readOnly) { + return base; + } + return [ + ...base, + gutter({ + class: 'cm-checkboxMarkerExtension', + renderEmptyElements: false, + lineMarker(view, line) { + return new DiffCheckboxMarker(view.state.doc.lineAt(line.from).number); + }, + lineMarkerChange: () => false, + widgetMarker: () => null, + }), + ]; +}; + +export const diffMarkerStateIconGutter = [ + gutter({ + class: 'cm-checkboxMarkerStateIconGutter', + renderEmptyElements: true, + lineMarker(view, line) { + return new DiffGutterMarkerStateIcon( + view.state.doc.lineAt(line.from).number, + ); + }, + lineMarkerChange: () => false, + widgetMarker: () => null, + }), +]; diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts new file mode 100644 index 000000000..061184cf1 --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts @@ -0,0 +1,90 @@ +import {StateEffect, StateField} from '@codemirror/state'; +import {Decoration, EditorView} from '@codemirror/view'; +import {events} from './events'; + +export const diffExtensionConstants = { + decoratedLineDataAttribute: 'data-diff-line', + lineInserted: 'line-inserted', + lineDeleted: 'line-deleted', +}; +export const insertedLineDecoration = Decoration.line({ + attributes: { + [diffExtensionConstants.decoratedLineDataAttribute]: + diffExtensionConstants.lineInserted, + }, +}); + +export const removedLineDecoration = Decoration.line({ + attributes: { + [diffExtensionConstants.decoratedLineDataAttribute]: + diffExtensionConstants.lineDeleted, + }, +}); + +export type DiffCheckboxState = 'added' | 'removed' | 'untouched'; + +type DiffLineEffect = {position: number; state: DiffCheckboxState | null}; + +export const diffLineEffect = StateEffect.define({ + map: (val, mapping) => ({ + position: mapping.mapPos(val.position), + state: val.state, + }), +}); + +export function dispatchUpdateDiffLineState( + view: EditorView, + lineNumber: number, + state: DiffCheckboxState, +) { + const line = view.state.doc.line(lineNumber); + view.dispatch({ + effects: diffLineEffect.of({position: line.from, state: state}), + }); +} + +export function getDecorationByState( + state: DiffCheckboxState | null, +): Decoration | null { + switch (state) { + case 'untouched': + return null; + case 'added': + return insertedLineDecoration; + case 'removed': + return removedLineDecoration; + default: + return null; + } +} + +export const diffLineState = StateField.define({ + create() { + return Decoration.none; + }, + update(decorationSet, transaction) { + decorationSet = decorationSet.map(transaction.changes); + for (const effect of transaction.effects) { + if (effect.is(diffLineEffect)) { + const line = transaction.state.doc.lineAt(effect.value.position); + const decoration = getDecorationByState(effect.value.state); + if (!decoration) { + decorationSet = decorationSet.update({ + filter: from => from !== line.from, + }); + } else { + decorationSet = decorationSet.update({ + add: [decoration.range(line.from, line.from)], + filter: from => from !== line.from, + }); + } + events.emit({ + state: effect.value.state, + line: line, + }); + } + } + return decorationSet; + }, + provide: field => EditorView.decorations.from(field) ?? Decoration.none, +}); diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffTheme.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/theme.ts similarity index 62% rename from apps/codeimage/src/components/CustomEditor/plugins/diff/diffTheme.ts rename to apps/codeimage/src/components/CustomEditor/plugins/diff/theme.ts index 71bc05bef..1704ca8be 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffTheme.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/theme.ts @@ -1,6 +1,7 @@ import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'; import {EditorView} from '@codemirror/view'; import {tags} from '@lezer/highlight'; +import {diffExtensionConstants} from './state'; export const colors = { removeLine: 'hsla(339, 100%, 57%, 25%)', @@ -9,12 +10,14 @@ export const colors = { addToken: 'hsl(120, 81%, 38%, 100%)', }; -export const diffTheme = [ +const dataAttrRemoved = `${diffExtensionConstants.decoratedLineDataAttribute}=${diffExtensionConstants.lineDeleted}`; +const dataAttrAdded = `${diffExtensionConstants.decoratedLineDataAttribute}=${diffExtensionConstants.lineInserted}`; +export const theme = [ EditorView.theme({ - '.cm-remove-line': { + [`.cm-line[${dataAttrRemoved}]`]: { backgroundColor: colors.removeLine, }, - '.cm-add-line': { + [`.cm-line[${dataAttrAdded}]`]: { backgroundColor: colors.addLine, }, }), diff --git a/apps/codeimage/src/components/CustomEditor/plugins/sync/sync.ts b/apps/codeimage/src/components/CustomEditor/plugins/sync/sync.ts new file mode 100644 index 000000000..320f56c05 --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/sync/sync.ts @@ -0,0 +1,37 @@ +import {Annotation, StateEffect, Transaction} from '@codemirror/state'; +import {EditorView} from '@codemirror/view'; +import {editorRegisteredEffects} from '../../registered-effects'; + +const syncAnnotation = Annotation.define(); + +export function syncDispatch(transaction: Transaction, other: EditorView) { + if (transaction.annotation(syncAnnotation)) { + return; + } + const annotations: Annotation[] = [syncAnnotation.of(true)]; + const effects: StateEffect[] = []; + let changed = false; + if (!transaction.changes.empty) { + changed = true; + const userEvent = transaction.annotation(Transaction.userEvent); + if (userEvent) annotations.push(Transaction.userEvent.of(userEvent)); + } + const stateEffects = transaction.effects + .filter(effect => !!effect) + .filter(effect => + editorRegisteredEffects.some(registeredEffect => + effect.is(registeredEffect), + ), + ); + if (stateEffects.length) { + changed = true; + effects.push(...stateEffects); + } + if (changed) { + other.dispatch({ + changes: transaction.changes, + effects, + annotations, + }); + } +} diff --git a/apps/codeimage/src/components/CustomEditor/registered-effects.ts b/apps/codeimage/src/components/CustomEditor/registered-effects.ts new file mode 100644 index 000000000..5ac0884cf --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/registered-effects.ts @@ -0,0 +1,3 @@ +import {diffLineEffect} from './plugins/diff/state'; + +export const editorRegisteredEffects = [diffLineEffect]; diff --git a/apps/codeimage/src/components/PropertyEditor/EditorStyleForm.tsx b/apps/codeimage/src/components/PropertyEditor/EditorStyleForm.tsx index 6832aa779..0749034ab 100644 --- a/apps/codeimage/src/components/PropertyEditor/EditorStyleForm.tsx +++ b/apps/codeimage/src/components/PropertyEditor/EditorStyleForm.tsx @@ -45,7 +45,7 @@ export const EditorStyleForm: ParentComponent = () => { setFontWeight, setFontId, setEnableLigatures, - setEnableDiff, + setShowDiff, }, computed: {selectedFont}, } = getRootEditorStore(); @@ -219,8 +219,8 @@ export const EditorStyleForm: ParentComponent = () => { > diff --git a/packages/highlight/README.md b/packages/highlight/README.md index c4ef6e3c1..a33c48325 100644 --- a/packages/highlight/README.md +++ b/packages/highlight/README.md @@ -28,7 +28,7 @@ It will ask you for a name of for the theme. The script will do the following: - Creates a new folder in the [`src/lib/themes`](./src/lib/themes) folder with the name you used. -- Generate a `index.ts` and `{{yourTheme}}.ts` file in the new folder. +- Generate a `extension.ts` and `{{yourTheme}}.ts` file in the new folder. - Automatically add the `export` and `typesVersion` entry in the [package.json](./package.json) ## Available themes diff --git a/packages/highlight/rollup.config.js b/packages/highlight/rollup.config.js index f5541daf1..eaa74aaaf 100644 --- a/packages/highlight/rollup.config.js +++ b/packages/highlight/rollup.config.js @@ -35,7 +35,7 @@ rmSync('dist', { export default defineConfig({ input: { index: 'src/public-api.ts', - themes: 'src/lib/themes/index.ts', + themes: 'src/lib/themes/extension.ts', ...inputs, }, external, diff --git a/packages/vanilla-extract/rollup.config.js b/packages/vanilla-extract/rollup.config.js index 1d95f0b99..07fb9fe86 100644 --- a/packages/vanilla-extract/rollup.config.js +++ b/packages/vanilla-extract/rollup.config.js @@ -17,13 +17,13 @@ rmSync('dist', { }); const paths = { - 'vite-plugin': 'src/vite-plugin/index.ts', + 'vite-plugin': 'src/vite-plugin/extension.ts', }; /** @type {import('rollup').RollupOptions} */ const options = { input: { - index: 'src/index.ts', + index: 'src/extension.ts', ...paths, }, external, From f7c6cfb9e861a9107e4978c84f0ff068bd83438f Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 3 Jan 2024 19:52:36 +0100 Subject: [PATCH 03/12] feat: diff style --- .../CustomEditor/plugins/diff/diffCheckboxMarker.tsx | 4 ++-- .../CustomEditor/plugins/diff/{events.ts => diffEvents.ts} | 2 +- .../CustomEditor/plugins/diff/diffMarkerStateIcon.tsx | 6 +++--- .../src/components/CustomEditor/plugins/diff/extension.ts | 4 ++-- .../src/components/CustomEditor/plugins/diff/state.ts | 4 ++-- apps/codeimage/src/components/Frame/PreviewFrame.tsx | 1 - 6 files changed, 10 insertions(+), 11 deletions(-) rename apps/codeimage/src/components/CustomEditor/plugins/diff/{events.ts => diffEvents.ts} (86%) diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx index 87da3d7bc..b07a5aea1 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx @@ -2,7 +2,7 @@ import {EditorView, GutterMarker} from '@codemirror/view'; import {createRoot, createSignal, onCleanup} from 'solid-js'; import {DiffCheckbox, DiffCheckboxState} from './DiffCheckbox'; import {container} from './DiffCheckbox.css'; -import {events} from './events'; +import {diffEvents} from './diffEvents'; import {dispatchUpdateDiffLineState} from './state'; export class DiffCheckboxMarker extends GutterMarker { @@ -27,7 +27,7 @@ export class DiffCheckboxMarker extends GutterMarker { this.dispose = dispose; const [value, setValue] = createSignal('untouched'); - const unsubscribe = events.listen(({state, line}) => { + const unsubscribe = diffEvents.listen(({state, line}) => { if (line.number === this.lineNumber) { setValue(state ?? 'untouched'); } diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/events.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts similarity index 86% rename from apps/codeimage/src/components/CustomEditor/plugins/diff/events.ts rename to apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts index f96ec55d5..cccffc178 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/events.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts @@ -3,7 +3,7 @@ import {createEventBus} from '@solid-primitives/event-bus'; import {createRoot} from 'solid-js'; import {DiffCheckboxState} from './DiffCheckbox'; -export const events = createRoot(() => +export const diffEvents = createRoot(() => createEventBus<{ state: DiffCheckboxState | null; line: Line; diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx index 8e0aad85f..6d5a1a594 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx @@ -2,7 +2,7 @@ import {backgroundColorVar} from '@codeimage/ui'; import {gutter, GutterMarker} from '@codemirror/view'; import {assignInlineVars} from '@vanilla-extract/dynamic'; import {createRoot, createSignal, onCleanup, Show} from 'solid-js'; -import {events} from './events'; +import {diffEvents} from './diffEvents'; import {colors} from './theme'; import {DiffCheckboxState} from './DiffCheckbox'; import * as styles from './diffMarkerStateIcon.css'; @@ -42,7 +42,7 @@ export class DiffGutterMarkerStateIcon extends GutterMarker { const [state, setState] = createSignal('untouched'); const currentSymbol = () => this.symbols[state()]; - const unsubscribe = events.listen(({state, line}) => { + const unsubscribe = diffEvents.listen(({state, line}) => { if (line.number === this.lineNumber) { setState(state ?? 'untouched'); } @@ -60,7 +60,7 @@ export class DiffGutterMarkerStateIcon extends GutterMarker { [backgroundColorVar]: currentSymbol().color, })} > - {currentSymbol()?.label} + {currentSymbol().label}
)} diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts index 051c35438..99edcc7e3 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts @@ -16,7 +16,7 @@ export const diffMarkerControl = (options: DiffMarkerExtensionOptions) => { return [ ...base, gutter({ - class: 'cm-checkboxMarkerExtension', + class: 'cm-diffMarkerControlGutter', renderEmptyElements: false, lineMarker(view, line) { return new DiffCheckboxMarker(view.state.doc.lineAt(line.from).number); @@ -29,7 +29,7 @@ export const diffMarkerControl = (options: DiffMarkerExtensionOptions) => { export const diffMarkerStateIconGutter = [ gutter({ - class: 'cm-checkboxMarkerStateIconGutter', + class: 'cm-diffMarkerStateIconGutter', renderEmptyElements: true, lineMarker(view, line) { return new DiffGutterMarkerStateIcon( diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts index 061184cf1..5b020829b 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts @@ -1,6 +1,6 @@ import {StateEffect, StateField} from '@codemirror/state'; import {Decoration, EditorView} from '@codemirror/view'; -import {events} from './events'; +import {diffEvents} from './diffEvents'; export const diffExtensionConstants = { decoratedLineDataAttribute: 'data-diff-line', @@ -78,7 +78,7 @@ export const diffLineState = StateField.define({ filter: from => from !== line.from, }); } - events.emit({ + diffEvents.emit({ state: effect.value.state, line: line, }); diff --git a/apps/codeimage/src/components/Frame/PreviewFrame.tsx b/apps/codeimage/src/components/Frame/PreviewFrame.tsx index c100ba361..f3cbc4078 100644 --- a/apps/codeimage/src/components/Frame/PreviewFrame.tsx +++ b/apps/codeimage/src/components/Frame/PreviewFrame.tsx @@ -40,7 +40,6 @@ function PreviewPortal(props: ParentProps) {
}>{props.children}
From 3138ade3554ddff5edec03da92b8843150482d28 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 4 Jan 2024 16:10:29 +0100 Subject: [PATCH 04/12] improve git diff state --- .../components/CustomEditor/CanvasEditor.tsx | 64 ++++- .../components/CustomEditor/CustomEditor.tsx | 24 +- .../CustomEditor/PreviewExportEditor.tsx | 29 ++- .../plugins/customEffectAnnotation.ts | 3 + .../plugins/diff/diffCheckboxMarker.tsx | 4 +- .../CustomEditor/plugins/diff/diffEvents.ts | 15 +- .../plugins/diff/diffMarkerStateIcon.tsx | 12 +- .../CustomEditor/plugins/diff/extension.ts | 28 +-- .../CustomEditor/plugins/diff/state.ts | 218 +++++++++++++++--- .../CustomEditor/plugins/sync/sync.ts | 23 +- .../CustomEditor/registered-effects.ts | 3 - apps/codeimage/src/state/editor/editor.ts | 31 ++- apps/codeimage/src/state/editor/model.ts | 7 + 13 files changed, 369 insertions(+), 92 deletions(-) create mode 100644 apps/codeimage/src/components/CustomEditor/plugins/customEffectAnnotation.ts delete mode 100644 apps/codeimage/src/components/CustomEditor/registered-effects.ts diff --git a/apps/codeimage/src/components/CustomEditor/CanvasEditor.tsx b/apps/codeimage/src/components/CustomEditor/CanvasEditor.tsx index c60ed646d..47acd8800 100644 --- a/apps/codeimage/src/components/CustomEditor/CanvasEditor.tsx +++ b/apps/codeimage/src/components/CustomEditor/CanvasEditor.tsx @@ -1,8 +1,10 @@ import {useI18n} from '@codeimage/locale'; import {getRootEditorStore} from '@codeimage/store/editor'; import {getActiveEditorStore} from '@codeimage/store/editor/activeEditor'; +import {EditorMetadataState} from '@codeimage/store/editor/model'; import {getUiStore} from '@codeimage/store/ui'; import {HStack, toast} from '@codeimage/ui'; +import {StateField, Transaction} from '@codemirror/state'; import {EditorView} from '@codemirror/view'; import {Button} from '@codeui/kit'; import { @@ -10,21 +12,29 @@ import { createEditorControlledValue, createEditorFocus, } from 'solid-codemirror'; -import {Accessor, createEffect, createSignal, on} from 'solid-js'; +import {Accessor, createEffect, createMemo, createSignal, on} from 'solid-js'; import {AppLocaleEntries} from '../../i18n'; import {SparklesIcon} from '../Icons/SparklesIcon'; import CustomEditor from './CustomEditor'; +import {customEffectAnnotation} from './plugins/customEffectAnnotation'; +import { + diffLineState, + dispatchReplaceDiffLineState, +} from './plugins/diff/state'; interface CanvasEditorProps { readOnly: boolean; } +const initMetadataAnnotationEvent = 'init-metadata'; + export default function CanvasEditor(props: CanvasEditorProps) { const [editorView, setEditorView] = createSignal(); const activeEditorStore = getActiveEditorStore(); const { state: editorState, - actions: {setFocused}, + actions: {setFocused, setTabMetadata}, + canvasEditorEvents, } = getRootEditorStore(); const {setFocused: editorSetFocused} = createEditorFocus( @@ -37,6 +47,24 @@ export default function CanvasEditor(props: CanvasEditorProps) { editorView, view => { if (!view) return; + + canvasEditorEvents.listen(transaction => { + if ( + !transaction.annotation(customEffectAnnotation) || + transaction.annotation(Transaction.userEvent) === + initMetadataAnnotationEvent + ) + return; + const state: Record< + keyof EditorMetadataState, + StateField + > = { + diff: diffLineState, + }; + const json = transaction.state.toJSON(state); + setTabMetadata(json); + }); + createEffect( on( () => editorState.options.focused, @@ -47,6 +75,37 @@ export default function CanvasEditor(props: CanvasEditorProps) { }, ), ); + + const metadata = createMemo(() => activeEditorStore.editor()?.metadata); + + let sendInitEvent = true; + createEffect( + on(editorView, editorView => { + if (!editorView) return; + createEffect( + on([metadata], ([metadata]) => { + if (!metadata) { + return; + } + const {diff} = metadata; + if (diff) { + dispatchReplaceDiffLineState( + editorView, + { + inserted: diff.inserted, + deleted: diff.deleted, + }, + sendInitEvent + ? { + userEvent: initMetadataAnnotationEvent, + } + : undefined, + ); + } + }), + ); + }), + ); }, {defer: true}, ), @@ -107,6 +166,7 @@ export default function CanvasEditor(props: CanvasEditorProps) { return ( void; onValueChange?: (value: string) => void; + dispatchTransaction: boolean; } export default function CustomEditor(props: VoidProps) { @@ -83,7 +84,9 @@ export default function CustomEditor(props: VoidProps) { createExtension, } = createCodeMirror({ value: editor()?.code, - onTransactionDispatched: tr => canvasEditorEvents.emit(tr), + onTransactionDispatched: props.dispatchTransaction + ? tr => canvasEditorEvents.emit(tr) + : undefined, onValueChange: props.onValueChange, }); @@ -136,8 +139,6 @@ export default function CustomEditor(props: VoidProps) { }, '.cm-cursor': { borderLeftWidth: '2px', - height: '21px', - transform: 'translateY(-10%)', }, }); @@ -173,13 +174,14 @@ export default function CustomEditor(props: VoidProps) { createExtension(() => customFontExtension()); createExtension(currentLanguage); createExtension(currentExtraLanguage); + + createExtension(() => + editorState.options.showDiffMode + ? diffMarkerControl({readOnly: props.readOnly}) + : [], + ); createExtension(() => { - return [ - editorState.options.showDiffMode - ? diffMarkerControl({readOnly: props.readOnly}) - : [], - editorState.options.showLineNumbers ? lineNumbers() : [], - ]; + return editorState.options.showLineNumbers ? lineNumbers() : []; }); createExtension(() => themeConfiguration()?.editorTheme || []); createExtension(baseTheme); diff --git a/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx b/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx index 97a8bf3f4..f3b327410 100644 --- a/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx +++ b/apps/codeimage/src/components/CustomEditor/PreviewExportEditor.tsx @@ -1,5 +1,7 @@ import {getRootEditorStore} from '@codeimage/store/editor'; +import {Transaction} from '@codemirror/state'; import {EditorView} from '@codemirror/view'; +import {createEventStack} from '@solid-primitives/event-bus'; import {createCompartmentExtension} from 'solid-codemirror'; import {createEffect, createSignal, lazy, on} from 'solid-js'; import {diffMarkerStateIconGutter} from './plugins/diff/extension'; @@ -13,15 +15,31 @@ interface PreviewExportEditorProps { export default function PreviewExportEditor(props: PreviewExportEditorProps) { const [editorView, setEditorView] = createSignal(); + const {canvasEditorEvents} = getRootEditorStore(); + const transactions = createEventStack(); + canvasEditorEvents.listen(tr => transactions.emit(tr)); + const sync = (tr: Transaction, editorView: EditorView) => { + try { + syncDispatch(tr, editorView); + editorView.requestMeasure(); + } catch (e) { + console.error(e); + } + }; + + let unsubscribe: VoidFunction; createEffect( on(editorView, editorView => { + if (unsubscribe) unsubscribe(); if (!editorView) return; - getRootEditorStore().canvasEditorEvents.listen(tr => { - setTimeout(() => { - syncDispatch(tr, editorView); - editorView.requestMeasure(); - }, 100); + transactions.value().forEach(transaction => { + sync(transaction, editorView); + transactions.setValue(trs => trs.filter(tr => tr !== transaction)); + }); + unsubscribe = transactions.listen(({event: transaction, remove}) => { + sync(transaction, editorView); + remove(); }); }), ); @@ -30,6 +48,7 @@ export default function PreviewExportEditor(props: PreviewExportEditorProps) { return ( { props.onSetEditorView(editorView); setEditorView(editorView); diff --git a/apps/codeimage/src/components/CustomEditor/plugins/customEffectAnnotation.ts b/apps/codeimage/src/components/CustomEditor/plugins/customEffectAnnotation.ts new file mode 100644 index 000000000..fa137cdd0 --- /dev/null +++ b/apps/codeimage/src/components/CustomEditor/plugins/customEffectAnnotation.ts @@ -0,0 +1,3 @@ +import {Annotation} from '@codemirror/state'; + +export const customEffectAnnotation = Annotation.define(); diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx index b07a5aea1..6436aba2f 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffCheckboxMarker.tsx @@ -2,7 +2,7 @@ import {EditorView, GutterMarker} from '@codemirror/view'; import {createRoot, createSignal, onCleanup} from 'solid-js'; import {DiffCheckbox, DiffCheckboxState} from './DiffCheckbox'; import {container} from './DiffCheckbox.css'; -import {diffEvents} from './diffEvents'; +import {diffPluginEvents} from './diffEvents'; import {dispatchUpdateDiffLineState} from './state'; export class DiffCheckboxMarker extends GutterMarker { @@ -27,7 +27,7 @@ export class DiffCheckboxMarker extends GutterMarker { this.dispose = dispose; const [value, setValue] = createSignal('untouched'); - const unsubscribe = diffEvents.listen(({state, line}) => { + const unsubscribe = diffPluginEvents.on('syncLine', ({state, line}) => { if (line.number === this.lineNumber) { setValue(state ?? 'untouched'); } diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts index cccffc178..783366454 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts @@ -1,11 +1,14 @@ import {Line} from '@codemirror/state'; -import {createEventBus} from '@solid-primitives/event-bus'; -import {createRoot} from 'solid-js'; +import { + createEmitter, + createEventStack, + createGlobalEmitter, +} from '@solid-primitives/event-bus'; import {DiffCheckboxState} from './DiffCheckbox'; +import {createRoot} from 'solid-js'; -export const diffEvents = createRoot(() => - createEventBus<{ - state: DiffCheckboxState | null; - line: Line; +export const diffPluginEvents = createRoot(() => + createGlobalEmitter<{ + syncLine: {state: DiffCheckboxState | null; line: Line}; }>(), ); diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx index 6d5a1a594..9756489ab 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx @@ -1,11 +1,11 @@ import {backgroundColorVar} from '@codeimage/ui'; -import {gutter, GutterMarker} from '@codemirror/view'; +import {GutterMarker} from '@codemirror/view'; import {assignInlineVars} from '@vanilla-extract/dynamic'; -import {createRoot, createSignal, onCleanup, Show} from 'solid-js'; -import {diffEvents} from './diffEvents'; -import {colors} from './theme'; +import {Show, createRoot, createSignal, onCleanup} from 'solid-js'; import {DiffCheckboxState} from './DiffCheckbox'; +import {diffPluginEvents} from './diffEvents'; import * as styles from './diffMarkerStateIcon.css'; +import {colors} from './theme'; interface MarkerStateSymbolOption { label: string; @@ -38,11 +38,11 @@ export class DiffGutterMarkerStateIcon extends GutterMarker { return createRoot(dispose => { // eslint-disable-next-line solid/reactivity this.dispose = dispose; - // TODO: add initial state const [state, setState] = createSignal('untouched'); const currentSymbol = () => this.symbols[state()]; + console.log('upda'); - const unsubscribe = diffEvents.listen(({state, line}) => { + const unsubscribe = diffPluginEvents.on('syncLine', ({state, line}) => { if (line.number === this.lineNumber) { setState(state ?? 'untouched'); } diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts index 99edcc7e3..a4d6e6d8d 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/extension.ts @@ -8,6 +8,20 @@ interface DiffMarkerExtensionOptions { readOnly: boolean; } +export const diffMarkerStateIconGutter = [ + gutter({ + class: 'cm-diffMarkerStateIconGutter', + renderEmptyElements: true, + lineMarker(view, line) { + return new DiffGutterMarkerStateIcon( + view.state.doc.lineAt(line.from).number, + ); + }, + lineMarkerChange: () => false, + widgetMarker: () => null, + }), +]; + export const diffMarkerControl = (options: DiffMarkerExtensionOptions) => { const base = [diffLineState, theme]; if (options.readOnly) { @@ -26,17 +40,3 @@ export const diffMarkerControl = (options: DiffMarkerExtensionOptions) => { }), ]; }; - -export const diffMarkerStateIconGutter = [ - gutter({ - class: 'cm-diffMarkerStateIconGutter', - renderEmptyElements: true, - lineMarker(view, line) { - return new DiffGutterMarkerStateIcon( - view.state.doc.lineAt(line.from).number, - ); - }, - lineMarkerChange: () => false, - widgetMarker: () => null, - }), -]; diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts index 5b020829b..989fd6beb 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/state.ts @@ -1,8 +1,19 @@ -import {StateEffect, StateField} from '@codemirror/state'; -import {Decoration, EditorView} from '@codemirror/view'; -import {diffEvents} from './diffEvents'; +import { + Annotation, + EditorState, + Line, + Range, + RangeSetBuilder, + StateEffect, + StateField, + TransactionSpec, +} from '@codemirror/state'; +import {Decoration, DecorationSet, EditorView} from '@codemirror/view'; +import {customEffectAnnotation} from '../customEffectAnnotation'; +import {diffPluginEvents} from './diffEvents'; export const diffExtensionConstants = { + name: 'diff', decoratedLineDataAttribute: 'data-diff-line', lineInserted: 'line-inserted', lineDeleted: 'line-deleted', @@ -23,23 +34,65 @@ export const removedLineDecoration = Decoration.line({ export type DiffCheckboxState = 'added' | 'removed' | 'untouched'; -type DiffLineEffect = {position: number; state: DiffCheckboxState | null}; +type DiffLineEffect = { + line: number; + state: DiffCheckboxState | null; +}; + +type DiffLineReplaceEffect = { + line: number; + state: DiffCheckboxState | null; +}; -export const diffLineEffect = StateEffect.define({ - map: (val, mapping) => ({ - position: mapping.mapPos(val.position), - state: val.state, - }), +export const diffLineUpdateWithResetAnnotation = Annotation.define(); + +export const diffLineReplacerEffect = StateEffect.define< + DiffLineReplaceEffect[] +>({ + map: values => + values.map(val => ({ + line: val.line, + state: val.state, + })), }); +export const diffLineEffect = StateEffect.define(); + export function dispatchUpdateDiffLineState( view: EditorView, lineNumber: number, state: DiffCheckboxState, ) { - const line = view.state.doc.line(lineNumber); view.dispatch({ - effects: diffLineEffect.of({position: line.from, state: state}), + effects: diffLineEffect.of({ + line: lineNumber, + state: state, + }), + annotations: [customEffectAnnotation.of(diffExtensionConstants.name)], + }); +} + +export function dispatchReplaceDiffLineState( + view: EditorView, + lineNumbersMap: {inserted: number[]; deleted: number[]}, + options: TransactionSpec = {}, +) { + const changes: DiffLineReplaceEffect[] = [ + ...lineNumbersMap.inserted.map(line => ({ + line: line, + state: 'added' as const, + })), + ...lineNumbersMap.deleted.map(line => ({ + line: line, + state: 'removed' as const, + })), + ].sort((a, b) => a.line - b.line); + + view.dispatch({ + userEvent: 'replace-diff-line', + effects: [diffLineReplacerEffect.of(changes)], + annotations: [customEffectAnnotation.of(diffExtensionConstants.name)], + ...options, }); } @@ -58,33 +111,142 @@ export function getDecorationByState( } } -export const diffLineState = StateField.define({ +export class DiffLineStateJsonValue { + readonly inserted: number[] = []; + readonly deleted: number[] = []; + + constructor(obj: {inserted: number[]; deleted: number[]}) { + this.inserted = obj.inserted; + this.deleted = obj.deleted; + } + + static fromJSON( + value: DiffLineStateJsonValue, + state: EditorState, + ): DecorationSet { + console.log('from json'); + const builder = new RangeSetBuilder(); + value.inserted.forEach(inserted => { + const line = state.doc.line(inserted); + const add = getDecorationByState('added'); + add && builder.add(line.from, line.from, add); + }); + value.deleted.forEach(removed => { + const line = state.doc.line(removed); + const remove = getDecorationByState('removed'); + remove && builder.add(line.from, line.from, remove); + }); + return builder.finish(); + } + + static toJSON( + value: DecorationSet, + state: EditorState, + ): DiffLineStateJsonValue { + const inserted: number[] = []; + const deleted: number[] = []; + value.between(0, state.doc.length, (from, to, decoration) => { + const attr = + decoration.spec?.attributes[ + diffExtensionConstants.decoratedLineDataAttribute + ]; + if (!attr) return; + const line = state.doc.lineAt(from); + if (attr === diffExtensionConstants.lineInserted) { + inserted.push(line.number); + } else if (attr === diffExtensionConstants.lineDeleted) { + deleted.push(line.number); + } + }); + return {inserted, deleted} as DiffLineStateJsonValue; + } +} + +type LineChange = {line: Line; state: DiffCheckboxState}; + +const pushChange = (changes: LineChange[], change: LineChange) => { + const existingChange = changes.findIndex( + existingChange => existingChange.line.number === change.line.number, + ); + if (existingChange !== -1) { + changes[existingChange] = change; + } else { + changes.push(change); + } +}; + +export const diffLineState = StateField.define({ create() { return Decoration.none; }, update(decorationSet, transaction) { decorationSet = decorationSet.map(transaction.changes); + const changes: LineChange[] = []; + const collect = (value: DiffLineEffect) => { + if (value.line > transaction.state.doc.lines || value.line < 0) { + return; + } + const line = transaction.state.doc.line(value.line); + pushChange(changes, {line, state: value.state ?? 'untouched'}); + }; for (const effect of transaction.effects) { - if (effect.is(diffLineEffect)) { - const line = transaction.state.doc.lineAt(effect.value.position); - const decoration = getDecorationByState(effect.value.state); - if (!decoration) { - decorationSet = decorationSet.update({ - filter: from => from !== line.from, - }); - } else { - decorationSet = decorationSet.update({ - add: [decoration.range(line.from, line.from)], - filter: from => from !== line.from, - }); - } - diffEvents.emit({ - state: effect.value.state, - line: line, + if (effect.is(diffLineReplacerEffect)) { + // Reset old lines not present in changes + const changedLines = effect.value.map(({line}) => line); + Array.from( + {length: transaction.state.doc.lines}, + (_, index) => index + 1, + ).forEach(ln => { + if (!changedLines.includes(ln)) { + diffPluginEvents.emit('syncLine', { + state: 'untouched', + line: transaction.state.doc.line(ln), + }); + } }); + decorationSet = Decoration.none; + effect.value.forEach(collect); + } else if (effect.is(diffLineEffect)) { + collect(effect.value); } } + + if (changes.length === 0) { + return decorationSet; + } + + const decorations = changes + // Decorations must be sorted by their position before to be added to the decorationSet + .sort((a, b) => a.line.number - b.line.number) + .reduce((acc, change) => { + if (change.state === 'added') { + return acc.concat( + insertedLineDecoration.range(change.line.from, change.line.from), + ); + } else if (change.state === 'removed') { + return acc.concat( + removedLineDecoration.range(change.line.from, change.line.from), + ); + } + return acc; + }, [] as Range[]); + + decorationSet = decorationSet.update({ + add: decorations, + filter: from => changes.some(change => change.line.from !== from), + }); + + changes.forEach(change => { + diffPluginEvents.emit('syncLine', change); + }); + return decorationSet; }, + fromJSON(value: DiffLineStateJsonValue, state): DecorationSet { + return DiffLineStateJsonValue.fromJSON(value, state); + }, + toJSON(value, state): DiffLineStateJsonValue { + return DiffLineStateJsonValue.toJSON(value, state); + }, provide: field => EditorView.decorations.from(field) ?? Decoration.none, }); diff --git a/apps/codeimage/src/components/CustomEditor/plugins/sync/sync.ts b/apps/codeimage/src/components/CustomEditor/plugins/sync/sync.ts index 320f56c05..590439a40 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/sync/sync.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/sync/sync.ts @@ -1,12 +1,15 @@ import {Annotation, StateEffect, Transaction} from '@codemirror/state'; import {EditorView} from '@codemirror/view'; -import {editorRegisteredEffects} from '../../registered-effects'; +import {customEffectAnnotation} from '../customEffectAnnotation'; -const syncAnnotation = Annotation.define(); +export const syncAnnotation = Annotation.define(); -export function syncDispatch(transaction: Transaction, other: EditorView) { +export function syncDispatch( + transaction: Transaction, + other: EditorView, +): boolean { if (transaction.annotation(syncAnnotation)) { - return; + return false; } const annotations: Annotation[] = [syncAnnotation.of(true)]; const effects: StateEffect[] = []; @@ -16,16 +19,9 @@ export function syncDispatch(transaction: Transaction, other: EditorView) { const userEvent = transaction.annotation(Transaction.userEvent); if (userEvent) annotations.push(Transaction.userEvent.of(userEvent)); } - const stateEffects = transaction.effects - .filter(effect => !!effect) - .filter(effect => - editorRegisteredEffects.some(registeredEffect => - effect.is(registeredEffect), - ), - ); - if (stateEffects.length) { + if (!!transaction.annotation(customEffectAnnotation)) { changed = true; - effects.push(...stateEffects); + effects.push(...transaction.effects); } if (changed) { other.dispatch({ @@ -34,4 +30,5 @@ export function syncDispatch(transaction: Transaction, other: EditorView) { annotations, }); } + return changed; } diff --git a/apps/codeimage/src/components/CustomEditor/registered-effects.ts b/apps/codeimage/src/components/CustomEditor/registered-effects.ts deleted file mode 100644 index 5ac0884cf..000000000 --- a/apps/codeimage/src/components/CustomEditor/registered-effects.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {diffLineEffect} from './plugins/diff/state'; - -export const editorRegisteredEffects = [diffLineEffect]; diff --git a/apps/codeimage/src/state/editor/editor.ts b/apps/codeimage/src/state/editor/editor.ts index 41b1db20d..8c69239c9 100644 --- a/apps/codeimage/src/state/editor/editor.ts +++ b/apps/codeimage/src/state/editor/editor.ts @@ -8,7 +8,7 @@ import type {Transaction} from '@codemirror/state'; import {appEnvironment} from '@core/configuration'; import {createEventBus} from '@solid-primitives/event-bus'; import {from, map, shareReplay} from 'rxjs'; -import {createSelector} from 'solid-js'; +import {createSelector, untrack} from 'solid-js'; import {SetStoreFunction} from 'solid-js/store'; import {defineStore, provideState} from 'statebuilder'; import {createCommand, withProxyCommands} from 'statebuilder/commands'; @@ -22,6 +22,12 @@ export function getInitialEditorState(): EditorState { code: appEnvironment.defaultState.editor.code, languageId: appEnvironment.defaultState.editor.languageId, formatter: null, + metadata: { + diff: { + deleted: [], + inserted: [], + }, + }, tab: { tabName: 'index.tsx', tabIcon: undefined, @@ -37,7 +43,7 @@ export function getInitialEditorUiOptions(): EditorUIOptions { fontWeight: appEnvironment.defaultState.editor.font.types[0].weight, focused: false, enableLigatures: true, - showDiff: true, + showDiffMode: true, }; } @@ -110,6 +116,9 @@ export function createEditorsStore() { languageId: editor.languageId, id: editor.id, code: editor.code, + metadata: { + diff: editor.metadata.diff, + }, })); return { options: {...state.options, ...persistedState.options}, @@ -120,6 +129,9 @@ export function createEditorsStore() { languageId: editor.languageId, tab: {tabName: editor.tabName}, id: editor.id, + metadata: { + diff: editor.metadata.diff, + }, }; }), }; @@ -142,6 +154,9 @@ export function createEditorsStore() { code: editor.code, tabName: editor.tab.tabName ?? '', id: editor.id, + metadata: { + diff: editor.metadata.diff, + }, }; }), options: { @@ -229,6 +244,16 @@ export function createEditorsStore() { store.dispatch(editorUpdateCommand, void 0); }; + const setTabMetadata = (data: EditorState['metadata']) => { + untrack(() => { + const index = store.get.editors.findIndex(tab => isActive(tab.id)); + setEditors(index, 'metadata', { + diff: data.diff, + }); + }); + store.dispatch(editorUpdateCommand, void 0); + }; + const configuredFonts = () => [ ...configStore.get.fonts, ...configStore.get.systemFonts, @@ -253,6 +278,7 @@ export function createEditorsStore() { languageId: editor.languageId, id: editor.id, code: editor.code, + metadata: editor.metadata, } as EditorState), ), ); @@ -289,6 +315,7 @@ export function createEditorsStore() { addEditor, removeEditor, setTabName, + setTabMetadata, setFromWorkspace, ...store.actions, setFontId(fontId: string) { diff --git a/apps/codeimage/src/state/editor/model.ts b/apps/codeimage/src/state/editor/model.ts index eb649140b..46a0581be 100644 --- a/apps/codeimage/src/state/editor/model.ts +++ b/apps/codeimage/src/state/editor/model.ts @@ -1,4 +1,5 @@ import {PersistedFrameState} from '@codeimage/store/frame/model'; +import {DiffLineStateJsonValue} from '../../components/CustomEditor/plugins/diff/state'; export interface EditorUIOptions { fontId: string; @@ -14,12 +15,17 @@ export interface TabState { tabIcon?: string; } +export interface EditorMetadataState { + diff: DiffLineStateJsonValue | null; +} + export interface EditorState { id: string; code: string; tab: TabState; formatter?: string | null; languageId: string; + metadata: EditorMetadataState; } export interface EditorUIOptions { @@ -38,6 +44,7 @@ export interface PersistedEditorState { code: string; tabName: string; languageId: string; + metadata: EditorState['metadata']; }[]; } From 94c2fb6d331b53019dfb0dc1c760cfbe729562be Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 4 Jan 2024 16:19:30 +0100 Subject: [PATCH 05/12] changes --- .../src/components/CustomEditor/CanvasEditor.tsx | 3 ++- .../components/CustomEditor/plugins/diff/diffEvents.ts | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/codeimage/src/components/CustomEditor/CanvasEditor.tsx b/apps/codeimage/src/components/CustomEditor/CanvasEditor.tsx index 47acd8800..766f5419c 100644 --- a/apps/codeimage/src/components/CustomEditor/CanvasEditor.tsx +++ b/apps/codeimage/src/components/CustomEditor/CanvasEditor.tsx @@ -78,7 +78,7 @@ export default function CanvasEditor(props: CanvasEditorProps) { const metadata = createMemo(() => activeEditorStore.editor()?.metadata); - let sendInitEvent = true; + let sendInitEvent = false; createEffect( on(editorView, editorView => { if (!editorView) return; @@ -102,6 +102,7 @@ export default function CanvasEditor(props: CanvasEditorProps) { : undefined, ); } + sendInitEvent = true; }), ); }), diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts index 783366454..123cef5d7 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffEvents.ts @@ -1,11 +1,7 @@ import {Line} from '@codemirror/state'; -import { - createEmitter, - createEventStack, - createGlobalEmitter, -} from '@solid-primitives/event-bus'; -import {DiffCheckboxState} from './DiffCheckbox'; +import {createGlobalEmitter} from '@solid-primitives/event-bus'; import {createRoot} from 'solid-js'; +import {DiffCheckboxState} from './DiffCheckbox'; export const diffPluginEvents = createRoot(() => createGlobalEmitter<{ From 7f9908c4db7d44a03df095aa59d21b6288e4f29e Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 4 Jan 2024 16:39:18 +0100 Subject: [PATCH 06/12] debug preview style --- apps/codeimage/src/components/Frame/Frame.css.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/codeimage/src/components/Frame/Frame.css.ts b/apps/codeimage/src/components/Frame/Frame.css.ts index 5c583988a..a6e7ac8b0 100644 --- a/apps/codeimage/src/components/Frame/Frame.css.ts +++ b/apps/codeimage/src/components/Frame/Frame.css.ts @@ -1,4 +1,5 @@ import {backgroundColorVar, themeVars, withThemeMode} from '@codeimage/ui'; +import {themeTokens} from '@codeui/kit'; import {createTheme, style} from '@vanilla-extract/css'; export const [frame, frameVars] = createTheme({ @@ -63,12 +64,25 @@ export const previewPortal = style({ height: 'auto', opacity: 0, transformOrigin: 'left top', + zoom: '50%', selectors: { '&[data-dev-mode]': { opacity: 1, zIndex: 999, }, }, + ':after': { + content: 'Debug preview', + position: 'absolute', + left: 0, + top: 0, + zIndex: 999, + borderRadius: themeTokens.radii.md, + padding: themeTokens.spacing['2'], + backgroundColor: '#333', + color: 'white', + margin: themeTokens.spacing['1'], + }, }); export const container = style([ From a6bc4498dfaa5a052855359e0fc9d0df3a565022 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 4 Jan 2024 16:44:04 +0100 Subject: [PATCH 07/12] fix --- packages/highlight/rollup.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/highlight/rollup.config.js b/packages/highlight/rollup.config.js index eaa74aaaf..f5541daf1 100644 --- a/packages/highlight/rollup.config.js +++ b/packages/highlight/rollup.config.js @@ -35,7 +35,7 @@ rmSync('dist', { export default defineConfig({ input: { index: 'src/public-api.ts', - themes: 'src/lib/themes/extension.ts', + themes: 'src/lib/themes/index.ts', ...inputs, }, external, From d19ce91e1fe9b7b7f2832da06820067780c0c400 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 4 Jan 2024 16:52:54 +0100 Subject: [PATCH 08/12] fix --- packages/vanilla-extract/rollup.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vanilla-extract/rollup.config.js b/packages/vanilla-extract/rollup.config.js index 07fb9fe86..1d95f0b99 100644 --- a/packages/vanilla-extract/rollup.config.js +++ b/packages/vanilla-extract/rollup.config.js @@ -17,13 +17,13 @@ rmSync('dist', { }); const paths = { - 'vite-plugin': 'src/vite-plugin/extension.ts', + 'vite-plugin': 'src/vite-plugin/index.ts', }; /** @type {import('rollup').RollupOptions} */ const options = { input: { - index: 'src/extension.ts', + index: 'src/index.ts', ...paths, }, external, From 0350352678722ba1e06519146fe23d375ccafddd Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 4 Jan 2024 16:53:32 +0100 Subject: [PATCH 09/12] fix --- packages/highlight/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/highlight/README.md b/packages/highlight/README.md index a33c48325..c4ef6e3c1 100644 --- a/packages/highlight/README.md +++ b/packages/highlight/README.md @@ -28,7 +28,7 @@ It will ask you for a name of for the theme. The script will do the following: - Creates a new folder in the [`src/lib/themes`](./src/lib/themes) folder with the name you used. -- Generate a `extension.ts` and `{{yourTheme}}.ts` file in the new folder. +- Generate a `index.ts` and `{{yourTheme}}.ts` file in the new folder. - Automatically add the `export` and `typesVersion` entry in the [package.json](./package.json) ## Available themes From 8c94522edf7373f3c7d01d2bd1dd9b19f2d941c1 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 4 Jan 2024 16:54:13 +0100 Subject: [PATCH 10/12] fix initial diff mode state --- apps/codeimage/src/state/editor/editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/codeimage/src/state/editor/editor.ts b/apps/codeimage/src/state/editor/editor.ts index 8c69239c9..5acd88838 100644 --- a/apps/codeimage/src/state/editor/editor.ts +++ b/apps/codeimage/src/state/editor/editor.ts @@ -43,7 +43,7 @@ export function getInitialEditorUiOptions(): EditorUIOptions { fontWeight: appEnvironment.defaultState.editor.font.types[0].weight, focused: false, enableLigatures: true, - showDiffMode: true, + showDiffMode: false, }; } From 147782e1e5c6d32243aa9371d96da5664059cb7a Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 4 Jan 2024 17:09:03 +0100 Subject: [PATCH 11/12] fix: remove console.log --- apps/codeimage/src/components/Footer/Footer.tsx | 12 ++++++++++-- apps/codeimage/src/state/editor/config.store.ts | 8 ++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/codeimage/src/components/Footer/Footer.tsx b/apps/codeimage/src/components/Footer/Footer.tsx index 1d0d826eb..d8c35fecc 100644 --- a/apps/codeimage/src/components/Footer/Footer.tsx +++ b/apps/codeimage/src/components/Footer/Footer.tsx @@ -1,15 +1,23 @@ import {Box, Link} from '@codeimage/ui'; import {createControlledDialog} from '@core/hooks/createControlledDialog'; import {Changelog} from '../Changelog/Changelog'; -import {link} from './Footer.css'; import * as styles from './Footer.css'; +import {link} from './Footer.css'; +import {Show} from 'solid-js'; +import {isDev} from 'solid-js/web'; export const Footer = () => { const openDialog = createControlledDialog(); - return (
+ + + window.toggleDevMode()} size="xs"> + Debug preview + + + getDefaultConfig()) .extend(withLocalFontManagementPlugin()) .extend(_ => { const fonts = createMemo(() => _.localFontsApi.state().fonts); - + const toggleDevMode = () => _.set('devMode', debug => !debug); onMount(() => { - window.toggleDevMode = () => _.set('devMode', debug => !debug); + window.toggleDevMode = toggleDevMode; }); const buildSystemFontConfiguration = (font: LoadedFont) => @@ -88,4 +88,8 @@ export const EditorConfigStore = defineStore(() => getDefaultConfig()) _.set('systemFonts', fontsConfiguration); }), ); + + return { + toggleDevMode, + }; }); From 52bab950f646ae821ba3b71edc955335e0f532dd Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Thu, 4 Jan 2024 17:21:40 +0100 Subject: [PATCH 12/12] add git diff extension color --- .../src/components/CustomEditor/CustomEditor.tsx | 5 ++++- .../plugins/diff/diffMarkerStateIcon.tsx | 1 - .../components/CustomEditor/plugins/diff/theme.ts | 14 ++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx b/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx index 007aeab8f..cc80d5f45 100644 --- a/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx +++ b/apps/codeimage/src/components/CustomEditor/CustomEditor.tsx @@ -36,6 +36,7 @@ import { } from 'solid-js'; import {createTabIcon} from '../../hooks/use-tab-icon'; import {diffMarkerControl} from './plugins/diff/extension'; +import {syntaxHighlightColorDiff} from './plugins/diff/theme'; const EDITOR_BASE_SETUP: Extension = [ highlightSpecialChars(), @@ -163,7 +164,9 @@ export default function CustomEditor(props: VoidProps) { }, }); }; - + createExtension(() => + selectedLanguage()?.id === 'git-patch' ? syntaxHighlightColorDiff : [], + ); createEditorReadonly(editorView, () => props.readOnly); createExtension(EditorView.lineWrapping); createExtension(() => diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx index 9756489ab..ea527265a 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/diffMarkerStateIcon.tsx @@ -40,7 +40,6 @@ export class DiffGutterMarkerStateIcon extends GutterMarker { this.dispose = dispose; const [state, setState] = createSignal('untouched'); const currentSymbol = () => this.symbols[state()]; - console.log('upda'); const unsubscribe = diffPluginEvents.on('syncLine', ({state, line}) => { if (line.number === this.lineNumber) { diff --git a/apps/codeimage/src/components/CustomEditor/plugins/diff/theme.ts b/apps/codeimage/src/components/CustomEditor/plugins/diff/theme.ts index 1704ca8be..5af145ac4 100644 --- a/apps/codeimage/src/components/CustomEditor/plugins/diff/theme.ts +++ b/apps/codeimage/src/components/CustomEditor/plugins/diff/theme.ts @@ -12,6 +12,7 @@ export const colors = { const dataAttrRemoved = `${diffExtensionConstants.decoratedLineDataAttribute}=${diffExtensionConstants.lineDeleted}`; const dataAttrAdded = `${diffExtensionConstants.decoratedLineDataAttribute}=${diffExtensionConstants.lineInserted}`; + export const theme = [ EditorView.theme({ [`.cm-line[${dataAttrRemoved}]`]: { @@ -21,10 +22,11 @@ export const theme = [ backgroundColor: colors.addLine, }, }), - syntaxHighlighting( - HighlightStyle.define([ - {tag: tags.inserted, color: colors.addToken}, - {tag: tags.deleted, color: colors.removeToken}, - ]), - ), ]; + +export const syntaxHighlightColorDiff = syntaxHighlighting( + HighlightStyle.define([ + {tag: [tags.inserted], color: colors.addToken}, + {tag: [tags.deleted], color: colors.removeToken}, + ]), +);