Skip to content

Commit ce7adf1

Browse files
authored
[UI] Enhance editor with sqlglot (#513)
1 parent 88ff1ff commit ce7adf1

15 files changed

Lines changed: 826 additions & 121 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ repos:
4242
- id: prettier
4343
name: prettier
4444
files: ^(web/client)
45-
entry: prettier --write
45+
entry: prettier --ignore-path web/client/.prettierignore
4646
exclude: ^(web/client/node_modules)
4747
require_serial: true
4848
language: node

web/client/.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
**/*.py
2+
.prettierignore

web/client/src/api/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
applyApiCommandsApplyPost,
2525
cancelPlanApiPlanCancelPost,
2626
type BodyApplyApiCommandsApplyPostCategories,
27+
getModelsApiModelsGet,
28+
type Models,
2729
} from './client'
2830

2931
export function useApiDag(): UseQueryResult<DagApiCommandsDagGet200> {
@@ -45,6 +47,15 @@ export function useApiFileByPath(path?: string): UseQueryResult<File> {
4547
})
4648
}
4749

50+
export function useApiModels(): UseQueryResult<Models> {
51+
return useQuery({
52+
queryKey: ['/api/models'],
53+
queryFn: async ({ signal }) => await getModelsApiModelsGet({ signal }),
54+
cacheTime: 0,
55+
enabled: false,
56+
})
57+
}
58+
4859
export function useApiFiles(): UseQueryResult<Directory> {
4960
return useQuery({
5061
queryKey: ['/api/files'],
@@ -161,3 +172,7 @@ export function apiCancelGetEnvironments(client: QueryClient): void {
161172
export function apiCancelFiles(client: QueryClient): void {
162173
void client.cancelQueries({ queryKey: ['/api/files'] })
163174
}
175+
176+
export function apiCancelModels(client: QueryClient): void {
177+
void client.cancelQueries({ queryKey: ['/api/models'] })
178+
}

web/client/src/context/fileTree.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isArrayNotEmpty, isFalse } from '~/utils'
44
import { ModelFile } from '../models'
55

66
interface FileTreeStore {
7-
files: ModelFile[]
7+
files: Map<ID, ModelFile>
88
activeFile: ModelFile
99
openedFiles: Set<ModelFile>
1010
setOpenedFiles: (files: Set<ModelFile>) => void
@@ -20,24 +20,27 @@ const [getOpenedFilesIds, setOpenedFilesIds] = useLocalStorage<{ ids: ID[] }>(
2020
)
2121

2222
export const useStoreFileTree = create<FileTreeStore>((set, get) => ({
23-
files: [],
23+
files: new Map(),
2424
activeFile: initialFile,
2525
openedFiles: new Set([initialFile]),
2626
setFiles(files: ModelFile[]) {
2727
set(() => {
2828
const openedFilesIds = getOpenedFilesIds()?.ids ?? []
2929
const openedFiles = new Set<ModelFile>([initialFile])
30+
const output = new Map()
3031

3132
if (isArrayNotEmpty(openedFilesIds)) {
3233
files.forEach(file => {
3334
if (openedFilesIds.includes(file.id)) {
3435
openedFiles.add(file)
3536
}
37+
38+
output.set(file.id, file)
3639
})
3740
}
3841

3942
return {
40-
files,
43+
files: output,
4144
openedFiles,
4245
}
4346
})
@@ -70,4 +73,4 @@ export const useStoreFileTree = create<FileTreeStore>((set, get) => ({
7073
activeFile: file,
7174
}))
7275
},
73-
}))
76+
}))
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useCallback, useEffect, useMemo, useState } from 'react'
2+
import CodeMirror from '@uiw/react-codemirror'
3+
import { python } from '@codemirror/lang-python'
4+
import { StreamLanguage } from '@codemirror/language'
5+
6+
import { yaml } from '@codemirror/legacy-modes/mode/yaml'
7+
import { type Extension } from '@codemirror/state'
8+
import { type Model } from '~/api/client'
9+
import { useStoreFileTree } from '~/context/fileTree'
10+
import { type ModelFile } from '~/models'
11+
import {
12+
events,
13+
SqlMeshModel,
14+
HoverTooltip,
15+
useSqlMeshExtension,
16+
} from './extensions'
17+
import { sqlglotWorker } from '~/library/components/editor/workers'
18+
import { dracula, tomorrow } from 'thememirror'
19+
import { useColorScheme, EnumColorScheme } from '~/context/theme'
20+
21+
export default function CodeEditor({
22+
file,
23+
models,
24+
dialect,
25+
dialects,
26+
onChange,
27+
}: {
28+
file: ModelFile
29+
models: Map<string, Model>
30+
dialect?: string
31+
dialects?: string[]
32+
onChange: (value: string) => void
33+
}): JSX.Element {
34+
const { mode } = useColorScheme()
35+
const theme = mode === EnumColorScheme.Dark ? dracula : tomorrow
36+
37+
const files = useStoreFileTree(s => s.files)
38+
const selectFile = useStoreFileTree(s => s.selectFile)
39+
40+
const [sqlDialectOptions, setSqlDialectOptions] = useState()
41+
42+
const [SqlMeshDialectExtension, SqlMeshDialectCleanUp] =
43+
useSqlMeshExtension(dialects)
44+
45+
const extensions = useMemo(() => {
46+
const showSqlSqlMeshDialect =
47+
file.extension === '.sql' && models != null && sqlDialectOptions != null
48+
return [
49+
models != null && HoverTooltip(models),
50+
models != null && events(models, files, selectFile),
51+
models != null && SqlMeshModel(models),
52+
showSqlSqlMeshDialect &&
53+
SqlMeshDialectExtension(models, file, sqlDialectOptions),
54+
file.extension === '.py' && python(),
55+
file.extension === '.yaml' && StreamLanguage.define(yaml),
56+
theme,
57+
].filter(Boolean) as Extension[]
58+
}, [file, models, sqlDialectOptions])
59+
60+
const handleSqlGlotWorkerMessage = useCallback((e: MessageEvent): void => {
61+
if (e.data.topic === 'dialect') {
62+
setSqlDialectOptions(e.data.payload)
63+
}
64+
}, [])
65+
66+
useEffect(() => {
67+
sqlglotWorker.addEventListener('message', handleSqlGlotWorkerMessage)
68+
69+
return () => {
70+
sqlglotWorker.removeEventListener('message', handleSqlGlotWorkerMessage)
71+
SqlMeshDialectCleanUp()
72+
}
73+
}, [])
74+
75+
useEffect(() => {
76+
sqlglotWorker.postMessage({
77+
topic: 'parse',
78+
payload: file.content,
79+
})
80+
}, [file.content, sqlglotWorker])
81+
82+
useEffect(() => {
83+
sqlglotWorker.postMessage({
84+
topic: 'dialect',
85+
payload: dialect,
86+
})
87+
}, [dialect, sqlglotWorker])
88+
89+
return (
90+
<CodeMirror
91+
value={file.content}
92+
height="100%"
93+
width="100%"
94+
className="w-full h-full overflow-auto text-sm"
95+
extensions={extensions}
96+
onChange={onChange}
97+
/>
98+
)
99+
}

web/client/src/library/components/editor/Editor.css

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
}
2727

2828
.cm-editor .cm-line::selection,
29-
.cm-editor .cm-line > *::selection {
29+
.cm-editor .cm-line>*::selection {
3030
background-color: var(--color-brand-100) !important;
3131
color: var(--color-brand-700) !important;
3232
}
@@ -44,3 +44,22 @@
4444
background: var(--color-brand);
4545
border-radius: 1rem;
4646
}
47+
48+
.sqlmesh-model {
49+
color: var(--color-primary-500);
50+
background: var(--color-primary-10);
51+
box-shadow: inset 0 -2px 0 0 var(--color-primary-300);
52+
display: inline-block;
53+
cursor: pointer;
54+
font-weight: bold;
55+
}
56+
57+
.cm-tooltip {
58+
border: none !important;
59+
outline: none !important;
60+
background: var(--color-theme) !important;
61+
}
62+
63+
.sqlmesh-model:hover {
64+
opacity: 0.8;
65+
}

0 commit comments

Comments
 (0)