| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- import Editor, { EditorProps, Monaco, OnChange, OnMount, useMonaco } from '@monaco-editor/react'
- import { merge, noop } from 'lodash'
- import type { editor } from 'monaco-editor'
- import { MutableRefObject, useEffect, useRef, useState } from 'react'
- import { cn, LogoLoader } from 'ui'
- import { alignEditor } from './CodeEditor.utils'
- import { Markdown } from '@/components/interfaces/Markdown'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { formatSql } from '@/lib/formatSql'
- import { timeout } from '@/lib/helpers'
- type CodeEditorActions = { enabled: boolean; callback: (value: any) => void }
- const DEFAULT_ACTIONS = {
- runQuery: { enabled: false, callback: noop },
- explainCode: { enabled: false, callback: noop },
- formatDocument: { enabled: true, callback: noop },
- placeholderFill: { enabled: true },
- closeAssistant: { enabled: false, callback: noop },
- }
- interface CodeEditorProps {
- id: string
- language: 'pgsql' | 'json' | 'html' | 'typescript' | undefined
- autofocus?: boolean
- defaultValue?: string
- isReadOnly?: boolean
- hideLineNumbers?: boolean
- className?: string
- loading?: boolean
- options?: EditorProps['options']
- value?: string
- placeholder?: string
- /* Determines what actions to add for code editor context menu */
- actions?: Partial<{
- runQuery: CodeEditorActions
- formatDocument: CodeEditorActions
- placeholderFill: Omit<CodeEditorActions, 'callback'>
- explainCode: CodeEditorActions
- closeAssistant: CodeEditorActions
- }>
- editorRef?: MutableRefObject<editor.IStandaloneCodeEditor | null>
- onInputChange?: (value?: string) => void
- }
- export const CodeEditor = ({
- id,
- language,
- defaultValue,
- autofocus = true,
- isReadOnly = false,
- hideLineNumbers = false,
- className,
- loading,
- options,
- value,
- placeholder,
- actions = DEFAULT_ACTIONS,
- editorRef: editorRefProps,
- onInputChange = noop,
- }: CodeEditorProps) => {
- const monaco = useMonaco()
- const { data: project } = useSelectedProjectQuery()
- const hasValue = useRef<editor.IContextKey<boolean>>(null)
- const ref = useRef<editor.IStandaloneCodeEditor>(null)
- const editorRef = editorRefProps || ref
- const monacoRef = useRef<Monaco>(null)
- const { runQuery, placeholderFill, formatDocument, explainCode, closeAssistant } = {
- ...DEFAULT_ACTIONS,
- ...actions,
- }
- const runQueryCallbackRef = useRef(runQuery.callback)
- useEffect(() => {
- runQueryCallbackRef.current = runQuery.callback
- }, [runQuery.callback])
- const showPlaceholderDefault = placeholder !== undefined && (value ?? '').trim().length === 0
- const [showPlaceholder, setShowPlaceholder] = useState(showPlaceholderDefault)
- const optionsMerged = merge(
- {
- tabSize: 2,
- fontSize: 13,
- readOnly: isReadOnly,
- minimap: { enabled: false },
- wordWrap: 'on',
- fixedOverflowWidgets: true,
- contextmenu: true,
- lineNumbers: hideLineNumbers ? 'off' : undefined,
- glyphMargin: hideLineNumbers ? false : undefined,
- lineNumbersMinChars: hideLineNumbers ? 0 : 4,
- folding: hideLineNumbers ? false : undefined,
- scrollBeyondLastLine: false,
- },
- options
- )
- const onMount: OnMount = async (editor, monaco) => {
- editorRef.current = editor
- monacoRef.current = monaco
- alignEditor(editor)
- hasValue.current = editor.createContextKey('hasValue', false)
- hasValue.current.set(value !== undefined && value.trim().length > 0)
- setShowPlaceholder(showPlaceholderDefault)
- if (placeholderFill.enabled) {
- editor.addCommand(
- monaco.KeyCode.Tab,
- () => {
- editor.executeEdits('source', [
- {
- // @ts-ignore
- identifier: 'add-placeholder',
- range: new monaco.Range(1, 1, 1, 1),
- text: (placeholder ?? '').split('\n\n').join('\n').replaceAll(' ', ' '),
- },
- ])
- },
- '!hasValue'
- )
- }
- if (runQuery.enabled) {
- editor.addAction({
- id: 'run-query',
- label: 'Run Query',
- keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter],
- contextMenuGroupId: 'operation',
- contextMenuOrder: 0,
- run: () => {
- const selectedValue = (editorRef?.current as any)
- .getModel()
- .getValueInRange((editorRef?.current as any)?.getSelection())
- runQueryCallbackRef.current(selectedValue || (editorRef?.current as any)?.getValue())
- },
- })
- }
- if (explainCode.enabled) {
- editor.addAction({
- id: 'explain-code',
- label: 'Explain Code',
- contextMenuGroupId: 'operation',
- contextMenuOrder: 1,
- run: () => {
- const selectedValue = (editorRef?.current as any)
- .getModel()
- .getValueInRange((editorRef?.current as any)?.getSelection())
- explainCode.callback(selectedValue)
- },
- })
- }
- if (closeAssistant.enabled) {
- editor.addAction({
- id: 'close-assistant',
- label: 'Close Assistant',
- keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyI],
- run: () => closeAssistant.callback(),
- })
- }
- const model = editor.getModel()
- if (model) {
- const position = model.getPositionAt((value ?? '').length)
- editor.setPosition(position)
- }
- await timeout(500)
- if (autofocus) editor?.focus()
- }
- const onChangeContent: OnChange = (value) => {
- if (hasValue.current) {
- hasValue.current.set((value ?? '').length > 0)
- }
- setShowPlaceholder(!value)
- onInputChange(value)
- }
- useEffect(() => {
- setShowPlaceholder(showPlaceholderDefault)
- }, [showPlaceholderDefault])
- useEffect(() => {
- if (
- placeholderFill.enabled &&
- editorRef.current !== undefined &&
- monacoRef.current !== undefined
- ) {
- const editor = editorRef.current
- if (editor == null) return
- const monaco = monacoRef.current
- if (monaco == null) return
- editor.addCommand(
- monaco.KeyCode.Tab,
- () => {
- editor.executeEdits('source', [
- {
- // @ts-ignore
- identifier: 'add-placeholder',
- range: new monaco.Range(1, 1, 1, 1),
- text: (placeholder ?? ' ')
- .split('\n\n')
- .join('\n')
- .replaceAll('*', '')
- .replaceAll(' ', ''),
- },
- ])
- },
- '!hasValue'
- )
- }
- }, [placeholder, placeholderFill.enabled])
- useEffect(() => {
- if (monaco && project && formatDocument.enabled) {
- const formatProvider = monaco.languages.registerDocumentFormattingEditProvider('pgsql', {
- async provideDocumentFormattingEdits(model: any) {
- const value = model.getValue()
- const formatted = formatSql(value)
- formatDocument.callback(formatted)
- return [{ range: model.getFullModelRange(), text: formatted }]
- },
- })
- return () => formatProvider.dispose()
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [monaco, project, formatDocument.enabled])
- return (
- <>
- <Editor
- path={id}
- theme="briven"
- className={cn(className, 'monaco-editor')}
- value={value ?? undefined}
- language={language}
- defaultValue={defaultValue ?? undefined}
- loading={loading || <LogoLoader />}
- options={optionsMerged}
- onMount={onMount}
- onChange={onChangeContent}
- />
- {placeholder !== undefined && (
- <div
- className={cn(
- 'monaco-placeholder absolute top-[3px] left-[57px] text-sm pointer-events-none font-mono',
- '[&>div>p]:text-foreground-lighter [&>div>p]:m-0! tracking-tighter',
- showPlaceholder ? 'block' : 'hidden'
- )}
- >
- <Markdown content={placeholder} />
- </div>
- )}
- </>
- )
- }
- export default CodeEditor
|