RLSCodeEditor.tsx 5.2 KB

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