Skip to content

Commit

Permalink
feat(hog): refactor code editors, hog validation (#23054)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra authored and timgl committed Jun 27, 2024
1 parent cdccce9 commit 0a7cf88
Show file tree
Hide file tree
Showing 35 changed files with 608 additions and 460 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
137 changes: 0 additions & 137 deletions frontend/src/lib/components/CodeEditors.tsx

This file was deleted.

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

import MonacoEditor, { type EditorProps, Monaco } from '@monaco-editor/react'
import { useMountedLogic, 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'> {
queryKey?: string
}
let codeEditorIndex = 0

export function CodeEditor({ queryKey, options, onMount, value, ...editorProps }: CodeEditorProps): JSX.Element {
const { isDarkModeOn } = useValues(themeLogic)
const scrollbarRendering = !inStorybookTestRunner() ? 'auto' : 'hidden'
const [monacoAndEditor, setMonacoAndEditor] = useState(
null as [Monaco, importedEditor.IStandaloneCodeEditor] | null
)
const [monaco, editor] = monacoAndEditor ?? []

const [realKey] = useState(() => codeEditorIndex++)
const builtCodeEditorLogic = codeEditorLogic({
key: queryKey ?? `new/${realKey}`,
query: value ?? '',
language: editorProps.language,
monaco: monaco,
editor: editor,
})
useMountedLogic(builtCodeEditorLogic)

// 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)
}
monacoDisposables.current.push(
monaco.languages.registerCodeActionProvider('hog', hogQLMetadataProvider(builtCodeEditorLogic))
)
}
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,
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 | undefined>(defaultHeight)

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 0a7cf88

Please sign in to comment.