RLSCodeEditor.tsx187 lines · main
1import Editor, { Monaco, OnChange, OnMount, useMonaco } from '@monaco-editor/react'
2import { noop } from 'lodash'
3import type { editor } from 'monaco-editor'
4import { MutableRefObject, useEffect, useRef } from 'react'
5import { cn } from 'ui'
6
7import { Markdown } from '@/components/interfaces/Markdown'
8import { formatSql } from '@/lib/formatSql'
9
10// [Joshen] Is there a way we can just have one single MonacoEditor component that's shared across the dashboard?
11// Feels like we're creating multiple copies of Editor. I'm keen to make this one the defacto as well so lets make sure
12// this component does not have RLS specific logic
13
14interface RLSCodeEditorProps {
15 id: string
16 defaultValue?: string
17 onInputChange?: (value?: string) => void
18 wrapperClassName?: string
19 className?: string
20 value?: string
21 placeholder?: string
22 readOnly?: boolean
23
24 disableTabToUsePlaceholder?: boolean
25 lineNumberStart?: number
26 onChange?: () => void
27 onMount?: () => void
28
29 editorRef: MutableRefObject<editor.IStandaloneCodeEditor | null>
30 monacoRef?: MutableRefObject<Monaco>
31}
32
33export const RLSCodeEditor = ({
34 id,
35 defaultValue,
36 onInputChange,
37 wrapperClassName,
38 className,
39 value,
40 placeholder,
41 readOnly = false,
42
43 disableTabToUsePlaceholder = false,
44 lineNumberStart,
45 onChange = noop,
46 onMount: _onMount = noop,
47
48 editorRef,
49 monacoRef,
50}: RLSCodeEditorProps) => {
51 const hasValue = useRef<editor.IContextKey<boolean>>(null)
52 const monaco = useMonaco()
53
54 const placeholderId = `monaco-placeholder-${id}`
55 const options: editor.IStandaloneEditorConstructionOptions = {
56 tabSize: 2,
57 fontSize: 13,
58 readOnly,
59 minimap: { enabled: false },
60 wordWrap: 'on' as const,
61 contextmenu: true,
62 lineNumbers:
63 lineNumberStart !== undefined ? (num) => (num + lineNumberStart).toString() : undefined,
64 glyphMargin: undefined,
65 lineNumbersMinChars: 4,
66 folding: undefined,
67 scrollBeyondLastLine: false,
68 }
69
70 const onMount: OnMount = async (editor, monaco) => {
71 editorRef.current = editor
72 if (monacoRef !== undefined) monacoRef.current = monaco
73
74 hasValue.current = editor.createContextKey('hasValue', false)
75 const placeholderEl = document.getElementById(placeholderId) as HTMLElement | null
76 if (placeholderEl && placeholder !== undefined && (value ?? '').trim().length === 0) {
77 placeholderEl.style.display = 'block'
78 }
79
80 if (!disableTabToUsePlaceholder) {
81 editor.addCommand(
82 monaco.KeyCode.Tab,
83 () => {
84 editor.executeEdits('source', [
85 {
86 // @ts-ignore
87 identifier: 'add-placeholder',
88 range: new monaco.Range(1, 1, 1, 1),
89 text: (placeholder ?? '')
90 .split('\n\n')
91 .join('\n')
92 .replaceAll('*', '')
93 .replaceAll('&nbsp;', ''),
94 },
95 ])
96 },
97 '!hasValue'
98 )
99 }
100
101 editor.focus()
102
103 _onMount()
104 }
105
106 const onChangeContent: OnChange = (value) => {
107 if (hasValue.current) {
108 hasValue.current.set((value ?? '').length > 0)
109 }
110
111 const placeholderEl = document.getElementById(placeholderId) as HTMLElement | null
112 if (placeholderEl) {
113 if (!value) {
114 placeholderEl.style.display = 'block'
115 } else {
116 placeholderEl.style.display = 'none'
117 }
118 }
119
120 onChange()
121 onInputChange?.(value)
122 }
123
124 // when the value has changed, trigger the onChange callback so that the height of the container can be adjusted.
125 // Happens when the value wordwraps and is updated via a template.
126 useEffect(() => {
127 onChange()
128 }, [value])
129
130 useEffect(() => {
131 if (monaco) {
132 // Enable pgsql format
133 const formatprovider = monaco.languages.registerDocumentFormattingEditProvider('pgsql', {
134 async provideDocumentFormattingEdits(model: any) {
135 const value = model.getValue()
136 const formatted = formatSql(value)
137 return [
138 {
139 range: model.getFullModelRange(),
140 text: formatted.trim(),
141 },
142 ]
143 },
144 })
145
146 return () => {
147 formatprovider.dispose()
148 }
149 }
150 }, [monaco])
151
152 useEffect(() => {
153 if (value !== undefined && value.trim().length > 0) {
154 const placeholderEl = document.getElementById(placeholderId) as HTMLElement | null
155 if (placeholderEl) placeholderEl.style.display = 'none'
156 }
157 }, [value])
158
159 return (
160 <>
161 <Editor
162 path={id}
163 theme="briven"
164 wrapperProps={{ className: cn(wrapperClassName) }}
165 className={cn(className, 'monaco-editor')}
166 value={value ?? undefined}
167 defaultLanguage="pgsql"
168 defaultValue={defaultValue ?? undefined}
169 options={options}
170 onMount={onMount}
171 onChange={onChangeContent}
172 />
173 {placeholder !== undefined && (
174 <div
175 id={placeholderId}
176 className={cn(
177 'monaco-placeholder absolute top-0 left-[57px] text-sm pointer-events-none font-mono tracking-tighter',
178 '[&>div>p]:text-foreground-lighter [&>div>p]:m-0!'
179 )}
180 style={{ display: 'none' }}
181 >
182 <Markdown content={placeholder} />
183 </div>
184 )}
185 </>
186 )
187}