- 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.
287 lines
7.5 KiB
JavaScript
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)
|
|
}
|