Tom Butcher 802de940ab Refactor CodeBlockEditor and Extract Utility Functions
- Moved the `toEditorCode` and `getCodeLanguageExtension` functions from CodeBlockEditor to a new utility file, `codeBlockEditorUtils.js`, for better code organization and reusability.
- Updated imports in CodeDiffViewer and DiffLabel components to reference the new utility file, ensuring consistent functionality across the application.
2026-08-21 11:08:42 +01:00

287 lines
7.5 KiB
JavaScript

import { useMemo, useState } from 'react'
import PropTypes from 'prop-types'
import CodeMirror from '@uiw/react-codemirror'
import { EditorView, Decoration, ViewPlugin } from '@codemirror/view'
import { StateField } from '@codemirror/state'
import {
farmControlDark,
farmControlLight
} from '../../../codemirror/farmControlTheme'
import {
getCodeLanguageExtension,
toEditorCode
} from './codeBlockEditorUtils'
import { useThemeContext } from '../context/ThemeContext'
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)
}
}
}
)
}
function buildErrorLineDecorations(state, errorLineSet) {
if (!errorLineSet.size) {
return Decoration.none
}
const decorations = []
for (let i = 1; i <= state.doc.lines; i++) {
if (errorLineSet.has(i)) {
const line = state.doc.line(i)
decorations.push(
Decoration.line({ class: 'cm-errorLine' }).range(line.from)
)
}
}
return Decoration.set(decorations, true)
}
function createErrorLineExtension(errorLines) {
const errorLineSet = new Set(errorLines)
return StateField.define({
create(state) {
return buildErrorLineDecorations(state, errorLineSet)
},
update(decorations, tr) {
if (tr.docChanged) {
return buildErrorLineDecorations(tr.state, errorLineSet)
}
return decorations.map(tr.changes)
},
provide: (f) => EditorView.decorations.from(f)
})
}
export default function CodeBlockEditor({
code = '',
value,
language = 'javascript',
style = {},
className = '',
readOnly = false,
onChange = null,
height = 'auto',
showLineNumbers = true,
disabled = false,
minimal = false,
autoCompleteObject = null,
errorLines = []
}) {
const { isDarkMode } = useThemeContext()
const [codeMirrorOpen, setCodeMirrorOpen] = useState(false)
const sourceCode = value !== undefined ? value : code
const editorCode = toEditorCode(sourceCode, language)
const languageExtension = useMemo(
() => getCodeLanguageExtension(language, autoCompleteObject),
[language, autoCompleteObject]
)
const errorLineExtension = useMemo(() => {
if (!errorLines?.length) {
return []
}
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))
} else {
onChange(value)
}
}
const codeMirror = (
<div
style={{
...(height !== 'auto' ? { height } : {}),
minHeight: 0,
overflow: 'visible',
...style
}}
className={`code-block-editor h-100 ${className}`.trim()}
>
<Card
style={{
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>
)
return (
<>
{minimal ? (
<div
style={{ maxWidth: '300px' }}
className='minimal-code-block-editor'
>
<Link
onClick={() => {
setCodeMirrorOpen(true)
}}
>
<pre>{editorCode.slice(-64).trim()}...</pre>
</Link>
<Modal
closeIcon={false}
open={codeMirrorOpen}
width={800}
footer={
<Button
onClick={() => {
setCodeMirrorOpen(false)
}}
>
Close
</Button>
}
>
{codeMirror}
</Modal>
</div>
) : (
codeMirror
)}
</>
)
}
CodeBlockEditor.propTypes = {
code: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
language: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
]),
style: PropTypes.object,
className: PropTypes.string,
readOnly: PropTypes.bool,
onChange: PropTypes.func,
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
showLineNumbers: PropTypes.bool,
disabled: PropTypes.bool,
minimal: PropTypes.bool,
autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
errorLines: PropTypes.arrayOf(PropTypes.number)
}