From 6a4074674d5bb117ffbb70fee0b6fa1e0d409960 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Thu, 20 Aug 2026 00:12:22 +0100 Subject: [PATCH] Implement custom CodeMirror themes and enhance CodeBlockEditor layout - Added farmControlLight and farmControlDark themes for CodeMirror integration, improving syntax highlighting and overall editor aesthetics. - Updated CodeBlockEditor to utilize the new themes and introduced a layout theme for better responsiveness. - Enhanced styling in App.css for the code block editor, ensuring proper display and overflow handling. - Refactored CodeDiffViewer to support the new themes, maintaining visual consistency across code comparison components. --- assets/stylesheets/App.css | 42 +--- src/codemirror/farmControlTheme.js | 230 ++++++++++++++++++ .../Dashboard/common/CodeBlockEditor.jsx | 168 ++++++++++--- .../Dashboard/common/CodeDiffViewer.jsx | 15 +- .../Dashboard/common/ObjectProperty.jsx | 1 + .../Dashboard/common/TemplateEditor.jsx | 19 +- 6 files changed, 399 insertions(+), 76 deletions(-) create mode 100644 src/codemirror/farmControlTheme.js diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 820b5764..433f3db1 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -47,7 +47,8 @@ } .ant-typography code, -.ant-typography pre { +.ant-typography pre, +.ͼ1 .cm-scroller { font-family: 'DM Mono'; } @@ -965,6 +966,17 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { margin: 0; } +.code-block-editor { + display: flex; + flex-direction: column; + min-height: 0; +} + +.code-block-editor > div { + flex: 1 1 auto; + min-height: 0; +} + .ant-splitter-panel:not(:first-child) .info-collapse .info-collapse-label { margin-left: 4px; } @@ -1316,31 +1328,3 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { background-color: #85858541; border-radius: 2px; } - -.ͼo { - color: var(--color-text); -} - -.ͼu { - color: var(--color-warning); -} - -.ͼq { - color: var(--color-error); -} - -.ͼ13 { - color: var(--color-success); -} - -.ͼp { - color: var(--color-purple); -} - -.ͼv { - color: var(--color-cyan); -} - -.ͼt { - color: var(--color-magenta); -} diff --git a/src/codemirror/farmControlTheme.js b/src/codemirror/farmControlTheme.js new file mode 100644 index 00000000..0f417f14 --- /dev/null +++ b/src/codemirror/farmControlTheme.js @@ -0,0 +1,230 @@ +import { EditorView } from '@codemirror/view' +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' +import { tags } from '@lezer/highlight' + +const editorFontFamily = "'DM Mono', monospace" + +const farmControlHighlightStyle = HighlightStyle.define([ + { tag: tags.keyword, color: 'var(--color-purple)' }, + { + tag: [ + tags.function(tags.variableName), + tags.labelName, + tags.definition(tags.function(tags.variableName)) + ], + color: 'var(--color-magenta)' + }, + { + tag: [tags.string, tags.inserted, tags.special(tags.string)], + color: 'var(--color-success)' + }, + { + tag: [ + tags.number, + tags.bool, + tags.atom, + tags.changed, + tags.typeName, + tags.className, + tags.annotation, + tags.modifier, + tags.self, + tags.namespace, + tags.color, + tags.constant(tags.name), + tags.standard(tags.name) + ], + color: 'var(--color-warning)' + }, + { + tag: [ + tags.operator, + tags.operatorKeyword, + tags.url, + tags.escape, + tags.regexp, + tags.link + ], + color: 'var(--color-cyan)' + }, + { tag: tags.invalid, color: 'var(--color-error)' }, + { + tag: [tags.meta, tags.comment], + color: 'color-mix(in srgb, var(--color-text) 55%, transparent)', + fontStyle: 'italic' + }, + { + tag: [ + tags.name, + tags.deleted, + tags.character, + tags.propertyName, + tags.macroName, + tags.definition(tags.name), + tags.separator, + tags.variableName + ], + color: 'var(--color-text)' + }, + { tag: tags.heading, fontWeight: 'bold', color: 'var(--color-error)' }, + { tag: tags.strong, fontWeight: 'bold' }, + { tag: tags.emphasis, fontStyle: 'italic' }, + { tag: tags.strikethrough, textDecoration: 'line-through' }, + { + tag: tags.link, + color: 'var(--color-link)', + textDecoration: 'underline' + }, + { + tag: [tags.processingInstruction], + color: 'var(--color-info)' + } +]) + +const farmControlLightTheme = EditorView.theme( + { + '&': { + color: 'var(--color-text)', + backgroundColor: 'var(--layout-modal-bg)', + fontFamily: editorFontFamily, + fontSize: '14px' + }, + '.cm-content': { + caretColor: 'var(--color-primary)' + }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: 'var(--color-primary)' + }, + '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': + { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 25%, transparent)' + }, + '.cm-activeLine': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 8%, transparent)' + }, + '.cm-selectionMatch': { + backgroundColor: + 'color-mix(in srgb, var(--color-success) 15%, transparent)' + }, + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + backgroundColor: + 'color-mix(in srgb, var(--color-warning) 20%, transparent)' + }, + '.cm-gutters': { + backgroundColor: 'var(--layout-modal-bg)', + color: 'color-mix(in srgb, var(--color-text) 45%, transparent)', + border: 'none' + }, + '.cm-activeLineGutter': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 8%, transparent)' + }, + '.cm-foldPlaceholder': { + backgroundColor: 'transparent', + border: 'none', + color: 'color-mix(in srgb, var(--color-text) 55%, transparent)' + }, + '.cm-tooltip': { + border: 'none', + backgroundColor: 'var(--layout-modal-bg)', + color: 'var(--color-text)' + }, + '.cm-tooltip .cm-tooltip-arrow:before': { + borderTopColor: 'transparent', + borderBottomColor: 'transparent' + }, + '.cm-tooltip .cm-tooltip-arrow:after': { + borderTopColor: 'var(--layout-modal-bg)', + borderBottomColor: 'var(--layout-modal-bg)' + }, + '.cm-tooltip-autocomplete': { + '& > ul > li[aria-selected]': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 12%, transparent)', + color: 'var(--color-text)' + } + } + }, + { dark: false } +) + +const farmControlDarkTheme = EditorView.theme( + { + '&': { + color: 'var(--color-text)', + backgroundColor: 'var(--layout-modal-bg)', + fontFamily: editorFontFamily, + fontSize: '14px' + }, + '.cm-content': { + caretColor: 'var(--color-primary)' + }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: 'var(--color-primary)' + }, + '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': + { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 30%, transparent)' + }, + '.cm-activeLine': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 10%, transparent)' + }, + '.cm-selectionMatch': { + backgroundColor: + 'color-mix(in srgb, var(--color-success) 18%, transparent)' + }, + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + backgroundColor: + 'color-mix(in srgb, var(--color-warning) 22%, transparent)' + }, + '.cm-gutters': { + backgroundColor: 'var(--layout-modal-bg)', + color: 'color-mix(in srgb, var(--color-text) 45%, transparent)', + border: 'none' + }, + '.cm-activeLineGutter': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 10%, transparent)' + }, + '.cm-foldPlaceholder': { + backgroundColor: 'transparent', + border: 'none', + color: 'color-mix(in srgb, var(--color-text) 55%, transparent)' + }, + '.cm-tooltip': { + border: 'none', + backgroundColor: 'var(--layout-modal-bg)', + color: 'var(--color-text)' + }, + '.cm-tooltip .cm-tooltip-arrow:before': { + borderTopColor: 'transparent', + borderBottomColor: 'transparent' + }, + '.cm-tooltip .cm-tooltip-arrow:after': { + borderTopColor: 'var(--layout-modal-bg)', + borderBottomColor: 'var(--layout-modal-bg)' + }, + '.cm-tooltip-autocomplete': { + '& > ul > li[aria-selected]': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 15%, transparent)', + color: 'var(--color-text)' + } + } + }, + { dark: true } +) + +export const farmControlLight = [ + farmControlLightTheme, + syntaxHighlighting(farmControlHighlightStyle) +] + +export const farmControlDark = [ + farmControlDarkTheme, + syntaxHighlighting(farmControlHighlightStyle) +] diff --git a/src/components/Dashboard/common/CodeBlockEditor.jsx b/src/components/Dashboard/common/CodeBlockEditor.jsx index 902ad6d0..bd82c3fc 100644 --- a/src/components/Dashboard/common/CodeBlockEditor.jsx +++ b/src/components/Dashboard/common/CodeBlockEditor.jsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react' import PropTypes from 'prop-types' import CodeMirror from '@uiw/react-codemirror' -import { EditorView, Decoration } from '@codemirror/view' +import { EditorView, Decoration, ViewPlugin } from '@codemirror/view' import { StateField } from '@codemirror/state' import { javascript } from '@codemirror/lang-javascript' import { python } from '@codemirror/lang-python' @@ -17,13 +17,88 @@ import { go } from '@codemirror/lang-go' import { php } from '@codemirror/lang-php' import { yaml } from '@codemirror/lang-yaml' import { xml } from '@codemirror/lang-xml' -import { oneDark } from '@codemirror/theme-one-dark' +import { + farmControlDark, + farmControlLight +} from '../../../codemirror/farmControlTheme' import { fcTemplateLang } from '../../../codemirror/fcTemplateLang' import { useThemeContext } from '../context/ThemeContext' -import { Button, Modal, Typography } from 'antd' +import ScrollBox from './ScrollBox' +import { Button, Modal, Typography, Card } from 'antd' const { Link } = Typography +const codeBlockEditorLayoutTheme = EditorView.theme({ + '&': { + height: 'auto !important' + }, + '.cm-scroller': { + overflow: 'visible !important' + }, + '.cm-gutters': { + position: 'sticky', + left: '0', + zIndex: 2 + } +}) + +function createScrollBoxSyncExtension() { + return ViewPlugin.fromClass( + class { + constructor(view) { + this.view = view + this.scrollEl = view.dom.closest('.simplebar-content-wrapper') + this.syncing = false + this.onExternalScroll = this.onExternalScroll.bind(this) + this.onEditorScroll = this.onEditorScroll.bind(this) + + if (this.scrollEl) { + this.scrollEl.addEventListener('scroll', this.onExternalScroll, { + passive: true + }) + view.scrollDOM.addEventListener('scroll', this.onEditorScroll, { + passive: true + }) + } + } + + syncFromExternal() { + const { view, scrollEl } = this + if (this.syncing || !scrollEl) return + this.syncing = true + view.scrollDOM.scrollTop = scrollEl.scrollTop + view.scrollDOM.scrollLeft = scrollEl.scrollLeft + view.scrollDOM.dispatchEvent(new Event('scroll')) + this.syncing = false + } + + syncFromEditor() { + const { view, scrollEl } = this + if (this.syncing || !scrollEl) return + this.syncing = true + scrollEl.scrollTop = view.scrollDOM.scrollTop + scrollEl.scrollLeft = view.scrollDOM.scrollLeft + this.syncing = false + } + + onExternalScroll() { + this.syncFromExternal() + } + + onEditorScroll() { + this.syncFromEditor() + } + + destroy() { + if (this.scrollEl) { + this.scrollEl.removeEventListener('scroll', this.onExternalScroll) + this.view.scrollDOM.removeEventListener('scroll', this.onEditorScroll) + } + } + } + ) +} + export function toEditorCode(code, language = 'javascript') { if (code == null) return '' if (typeof code === 'object' && language === 'json') { @@ -148,6 +223,16 @@ export default function CodeBlockEditor({ return [createErrorLineExtension(errorLines)] }, [errorLines]) + const editorExtensions = useMemo( + () => [ + languageExtension, + ...errorLineExtension, + codeBlockEditorLayoutTheme, + createScrollBoxSyncExtension() + ], + [languageExtension, errorLineExtension] + ) + const handleOnChange = (value) => { if (typeof code == 'object' && language == 'json') { onChange(JSON.parse(value)) @@ -159,43 +244,56 @@ export default function CodeBlockEditor({ const codeMirror = (
- + styles={{ body: { padding: 0, height: '100%', overflow: 'visible' } }} + > + + + +
) diff --git a/src/components/Dashboard/common/CodeDiffViewer.jsx b/src/components/Dashboard/common/CodeDiffViewer.jsx index 801c7abf..a82497f7 100644 --- a/src/components/Dashboard/common/CodeDiffViewer.jsx +++ b/src/components/Dashboard/common/CodeDiffViewer.jsx @@ -3,7 +3,10 @@ import PropTypes from 'prop-types' import { MergeView } from '@codemirror/merge' import { EditorState } from '@codemirror/state' import { EditorView } from '@codemirror/view' -import { oneDark } from '@codemirror/theme-one-dark' +import { + farmControlDark, + farmControlLight +} from '../../../codemirror/farmControlTheme' import { basicSetup } from 'codemirror' import { useThemeContext } from '../context/ThemeContext' import { @@ -29,20 +32,12 @@ export default function CodeDiffViewer({ useLayoutEffect(() => { if (!parentRef.current) return - const editorTheme = EditorView.theme({ - '&': { - fontSize: '14px', - fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace' - } - }) - const sharedExtensions = [ basicSetup, languageExtension, - editorTheme, EditorView.editable.of(false), EditorState.readOnly.of(true), - ...(isDarkMode ? [oneDark] : []) + ...(isDarkMode ? farmControlDark : farmControlLight) ] const view = new MergeView({ diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx index 8ccb4548..de3594b7 100644 --- a/src/components/Dashboard/common/ObjectProperty.jsx +++ b/src/components/Dashboard/common/ObjectProperty.jsx @@ -913,6 +913,7 @@ const ObjectProperty = ({ minimal={minimal} autoCompleteObject={autoCompleteObject} errorLines={errorLines} + {...(onChange && !useFormItem ? { onChange } : {})} {...inputProps} /> ) diff --git a/src/components/Dashboard/common/TemplateEditor.jsx b/src/components/Dashboard/common/TemplateEditor.jsx index f78a06ea..0d90ffd4 100644 --- a/src/components/Dashboard/common/TemplateEditor.jsx +++ b/src/components/Dashboard/common/TemplateEditor.jsx @@ -240,17 +240,32 @@ const TemplateEditor = ({
{ + form?.setFieldValue?.('content', value) + setObjectData?.((prev) => ({ + ...prev, + content: value + })) + }} isEditing={isEditing} autoCompleteObject={objectData?.testObject} errorLines={errorLines}