Skip to content

Commit

Permalink
refactor(hog): code editors
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra committed Jun 18, 2024
1 parent 8386504 commit 1746f94
Show file tree
Hide file tree
Showing 25 changed files with 549 additions and 435 deletions.
137 changes: 0 additions & 137 deletions frontend/src/lib/components/CodeEditors.tsx

This file was deleted.

File renamed without changes.
106 changes: 106 additions & 0 deletions frontend/src/lib/monaco/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import './CodeEditor.scss'

import MonacoEditor, { type EditorProps, Monaco } from '@monaco-editor/react'
import { useValues } from 'kea'
import { Spinner } from 'lib/lemon-ui/Spinner'
import { codeEditorLogic } from 'lib/monaco/codeEditorLogic'
import { hogQLAutocompleteProvider } from 'lib/monaco/hogQLAutocompleteProvider'
import { hogQLMetadataProvider } from 'lib/monaco/hogQLMetadataProvider'
import * as hog from 'lib/monaco/languages/hog'
import * as hogQL from 'lib/monaco/languages/hogql'
import { inStorybookTestRunner } from 'lib/utils'
import type { editor as importedEditor, IDisposable } from 'monaco-editor'
import { useEffect, useRef, useState } from 'react'

import { themeLogic } from '~/layout/navigation-3000/themeLogic'

export interface CodeEditorProps extends Omit<EditorProps, 'loading' | 'theme'> {
logicKey?: string
}
let codeEditorIndex = 0

export function CodeEditor({ logicKey, options, onMount, value, ...editorProps }: CodeEditorProps): JSX.Element {
const { isDarkModeOn } = useValues(themeLogic)
const scrollbarRendering = !inStorybookTestRunner() ? 'auto' : 'hidden'
const [realKey] = useState(() => codeEditorIndex++)
const [monacoAndEditor, setMonacoAndEditor] = useState(
null as [Monaco, importedEditor.IStandaloneCodeEditor] | null
)
const [monaco, editor] = monacoAndEditor ?? []
const builtCodeEditorLogic = codeEditorLogic({
key: logicKey ?? `new/${realKey}`,
query: value ?? '',
language: editorProps.language,
monaco: monaco,
editor: editor,
})

// Using useRef, not useState, as we don't want to reload the component when this changes.
const monacoDisposables = useRef([] as IDisposable[])
useEffect(() => {
return () => {
monacoDisposables.current.forEach((d) => d?.dispose())
}
}, [])

return (
<MonacoEditor // eslint-disable-line react/forbid-elements
theme={isDarkModeOn ? 'vs-dark' : 'vs-light'}
loading={<Spinner />}
options={{
// :TRICKY: We need to declare all options here, as omitting something will carry its value from one <CodeEditor> to another.
minimap: {
enabled: false,
},
scrollBeyondLastLine: false,
automaticLayout: true,
fixedOverflowWidgets: true,
wordWrap: 'off',
lineNumbers: 'on',
...options,
padding: { bottom: 8, top: 8 },
scrollbar: {
vertical: scrollbarRendering,
horizontal: scrollbarRendering,
...options?.scrollbar,
},
}}
value={value}
{...editorProps}
onMount={(editor, monaco) => {
setMonacoAndEditor([monaco, editor])
if (editorProps?.language === 'hog') {
if (!monaco.languages.getLanguages().some(({ id }) => id === 'hog')) {
monaco.languages.register({ id: 'hog', extensions: ['.hog'], mimetypes: ['application/hog'] })
monaco.languages.setLanguageConfiguration('hog', hog.conf)
monaco.languages.setMonarchTokensProvider('hog', hog.language)
}
}
if (editorProps?.language === 'hogql') {
if (!monaco.languages.getLanguages().some(({ id }) => id === 'hogql')) {
monaco.languages.register({
id: 'hogql',
extensions: ['.sql', '.hogql'],
mimetypes: ['application/hog+ql'],
})
monaco.languages.setLanguageConfiguration('hogql', hogQL.conf)
monaco.languages.setMonarchTokensProvider('hogql', hogQL.language)
}
monacoDisposables.current.push(
monaco.languages.registerCompletionItemProvider(
'hogql',
hogQLAutocompleteProvider(builtCodeEditorLogic)
)
)
monacoDisposables.current.push(
monaco.languages.registerCodeActionProvider(
'hogql',
hogQLMetadataProvider(builtCodeEditorLogic)
)
)
}
onMount?.(editor, monaco)
}}
/>
)
}
66 changes: 66 additions & 0 deletions frontend/src/lib/monaco/CodeEditorResizable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { CodeEditor, CodeEditorProps } from 'lib/monaco/CodeEditor'
import { useEffect, useRef, useState } from 'react'
import AutoSizer from 'react-virtualized/dist/es/AutoSizer'

export function CodeEditorResizeable({
height: defaultHeight = 200,
minHeight = '5rem',
maxHeight = '90vh',
...props
}: Omit<CodeEditorProps, 'height'> & {
height?: number
minHeight?: string | number
maxHeight?: string | number
}): JSX.Element {
const [height, setHeight] = useState(defaultHeight)
const [manualHeight, setManualHeight] = useState<number>()

const ref = useRef<HTMLDivElement | null>(null)

useEffect(() => {
const value = typeof props.value !== 'string' ? JSON.stringify(props.value, null, 2) : props.value
const lineCount = (value?.split('\n').length ?? 1) + 1
const lineHeight = 18
setHeight(lineHeight * lineCount)
}, [props.value])

return (
<div
ref={ref}
className="CodeEditorResizeable relative border rounded"
// eslint-disable-next-line react/forbid-dom-props
style={{
minHeight,
maxHeight,
height: manualHeight ?? height,
}}
>
<AutoSizer disableWidth>
{({ height }) => (
<CodeEditor
{...props}
height={height - 2} // Account for border
/>
)}
</AutoSizer>

{/* Using a standard resize css means we need overflow-hidden which hides parts of the editor unnecessarily */}
<div
className="absolute bottom-0 right-0 z-10 resize-y h-5 w-5 cursor-s-resize overflow-hidden"
onMouseDown={(e) => {
const startY = e.clientY
const startHeight = ref.current?.clientHeight ?? 0
const onMouseMove = (event: MouseEvent): void => {
setManualHeight(startHeight + event.clientY - startY)
}
const onMouseUp = (): void => {
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
}
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
}}
/>
</div>
)
}
Loading

0 comments on commit 1746f94

Please sign in to comment.