Implement custom CodeMirror themes and enhance CodeBlockEditor layout
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- 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.
This commit is contained in:
parent
84adf58542
commit
6a4074674d
@ -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);
|
||||
}
|
||||
|
||||
230
src/codemirror/farmControlTheme.js
Normal file
230
src/codemirror/farmControlTheme.js
Normal file
@ -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)
|
||||
]
|
||||
@ -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 = (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #85858541',
|
||||
...(height !== 'auto' ? { height } : {}),
|
||||
minHeight: 0,
|
||||
overflow: 'visible',
|
||||
...style
|
||||
}}
|
||||
className={className}
|
||||
className={`code-block-editor h-100 ${className}`.trim()}
|
||||
>
|
||||
<CodeMirror
|
||||
value={editorCode}
|
||||
height={height}
|
||||
theme={isDarkMode ? oneDark : 'light'}
|
||||
extensions={[languageExtension, ...errorLineExtension]}
|
||||
readOnly={readOnly || disabled}
|
||||
onChange={handleOnChange}
|
||||
basicSetup={{
|
||||
lineNumbers: showLineNumbers,
|
||||
foldGutter: true,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: false,
|
||||
syntaxHighlighting: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: !readOnly,
|
||||
rectangularSelection: false,
|
||||
crosshairCursor: false,
|
||||
highlightActiveLine: !readOnly,
|
||||
highlightSelectionMatches: false,
|
||||
closeBracketsKeymap: false,
|
||||
searchKeymap: false,
|
||||
foldKeymap: false,
|
||||
completionKeymap: !readOnly,
|
||||
lintKeymap: false
|
||||
}}
|
||||
<Card
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace'
|
||||
borderRadius: 0,
|
||||
height: '100%',
|
||||
overflow: 'visible',
|
||||
backgroundColor: 'var(--layout-modal-bg)'
|
||||
}}
|
||||
/>
|
||||
styles={{ body: { padding: 0, height: '100%', overflow: 'visible' } }}
|
||||
>
|
||||
<ScrollBox>
|
||||
<CodeMirror
|
||||
value={editorCode}
|
||||
height='auto'
|
||||
theme={isDarkMode ? farmControlDark : farmControlLight}
|
||||
extensions={editorExtensions}
|
||||
readOnly={readOnly || disabled}
|
||||
onChange={handleOnChange}
|
||||
basicSetup={{
|
||||
lineNumbers: showLineNumbers,
|
||||
foldGutter: true,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: false,
|
||||
syntaxHighlighting: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: !readOnly,
|
||||
rectangularSelection: false,
|
||||
crosshairCursor: false,
|
||||
highlightActiveLine: !readOnly,
|
||||
highlightSelectionMatches: false,
|
||||
closeBracketsKeymap: false,
|
||||
searchKeymap: false,
|
||||
foldKeymap: false,
|
||||
completionKeymap: !readOnly,
|
||||
lintKeymap: false
|
||||
}}
|
||||
style={{
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</ScrollBox>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -913,6 +913,7 @@ const ObjectProperty = ({
|
||||
minimal={minimal}
|
||||
autoCompleteObject={autoCompleteObject}
|
||||
errorLines={errorLines}
|
||||
{...(onChange && !useFormItem ? { onChange } : {})}
|
||||
{...inputProps}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -240,17 +240,32 @@ const TemplateEditor = ({
|
||||
|
||||
<div
|
||||
style={{
|
||||
overflowY: 'scroll',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flex: '1 1 auto'
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0
|
||||
}}
|
||||
>
|
||||
<ObjectProperty
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0
|
||||
}}
|
||||
useFormItem={false}
|
||||
name={'content'}
|
||||
height='100%'
|
||||
type='codeBlock'
|
||||
language={'fcTemplateLang'}
|
||||
objectData={objectData}
|
||||
onChange={(value) => {
|
||||
form?.setFieldValue?.('content', value)
|
||||
setObjectData?.((prev) => ({
|
||||
...prev,
|
||||
content: value
|
||||
}))
|
||||
}}
|
||||
isEditing={isEditing}
|
||||
autoCompleteObject={objectData?.testObject}
|
||||
errorLines={errorLines}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user