Skip to content

Commit 7890c09

Browse files
authored
Feat: Add better autocomplete for models and columns (#1607)
1 parent 5fd351c commit 7890c09

File tree

5 files changed

+169
-116
lines changed

5 files changed

+169
-116
lines changed

web/client/src/index.css

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,6 @@
22
@tailwind components;
33
@tailwind utilities;
44

5-
@layer base {
6-
.scrollbar--horizontal::-webkit-scrollbar {
7-
height: 0.2rem;
8-
}
9-
10-
.scrollbar--horizontal-md::-webkit-scrollbar {
11-
height: 0.4rem;
12-
}
13-
14-
.scrollbar--vertical::-webkit-scrollbar {
15-
width: 0.2rem;
16-
}
17-
18-
.scrollbar--vertical-md::-webkit-scrollbar {
19-
width: 0.4rem;
20-
}
21-
22-
.scrollbar::-webkit-scrollbar-track {
23-
background-color: transparent;
24-
}
25-
26-
.scrollbar::-webkit-scrollbar-thumb {
27-
background: var(--color-brand);
28-
border-radius: 1rem;
29-
}
30-
}
31-
32-
@layer components {
33-
.input-ring {
34-
@apply ring-accent-200 ring-offset-accent-500;
35-
}
36-
37-
.input-ring:focus {
38-
@apply outline-none ring-offset-2 ring-4;
39-
}
40-
}
41-
425
@layer base {
436
:root {
447
font-synthesis: none;
@@ -194,6 +157,37 @@
194157
/* General */
195158
--unit: 16px;
196159
--leading: 1.5;
160+
161+
--scrollbar-size: 6px;
162+
--scrollbar-radius: 1rem;
163+
--scrollbar-backgroud: var(--color-brand);
164+
}
165+
166+
.scrollbar--horizontal::-webkit-scrollbar {
167+
height: var(--scrollbar-size);
168+
}
169+
170+
.scrollbar--vertical::-webkit-scrollbar {
171+
width: var(--scrollbar-size);
172+
}
173+
174+
.scrollbar::-webkit-scrollbar-track {
175+
background: transparent;
176+
}
177+
178+
.scrollbar::-webkit-scrollbar-thumb {
179+
background: var(--scrollbar-backgroud);
180+
border-radius: var(--scrollbar-radius);
181+
}
182+
}
183+
184+
@layer components {
185+
.input-ring {
186+
@apply ring-accent-200 ring-offset-accent-500;
187+
}
188+
189+
.input-ring:focus {
190+
@apply outline-none ring-offset-2 ring-4;
197191
}
198192
}
199193

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

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44
color: var(--color-editor-text);
55
}
66

7-
.cm-editor .cm-scroller {
8-
font-family: inherit;
9-
}
10-
117
.cm-editor .cm-gutters {
128
background-color: var(--color-editor);
139
border: none;
@@ -31,29 +27,61 @@
3127
color: var(--color-brand-700) !important;
3228
}
3329

34-
.cm-editor .cm-scroller::-webkit-scrollbar {
35-
height: 0.2rem;
36-
width: 0.2rem;
30+
.cm-editor .cm-tooltip.cm-tooltip-autocomplete {
31+
border: 1px solid var(--color-neutral-300) !important;
32+
border-radius: 0.25rem;
33+
}
34+
35+
.cm-editor .cm-tooltip-autocomplete ul li[aria-selected='true'] {
36+
background: var(--color-primary-10);
37+
color: var(--color-text);
38+
}
39+
40+
.cm-editor .cm-tooltip-autocomplete .cm-completionIcon-column,
41+
.cm-editor .cm-tooltip-autocomplete .cm-completionIcon-model {
42+
margin: 0.125rem 1.25rem 0.125rem 0;
43+
}
44+
45+
.cm-editor .cm-tooltip-autocomplete .cm-completionIcon::before {
46+
display: inline-block;
47+
padding: 0.125rem 0.25rem;
48+
border-radius: 0.25rem;
49+
}
50+
51+
.cm-editor .cm-tooltip-autocomplete .cm-completionIcon-column::before {
52+
content: 'col';
53+
color: var(--color-brand-600);
54+
background: var(--color-brand-10);
55+
}
56+
57+
.cm-editor .cm-tooltip-autocomplete .cm-completionIcon-model::before {
58+
content: 'mdl';
59+
color: var(--color-primary-500);
60+
background: var(--color-primary-10);
3761
}
3862

39-
.scrollbar--vertical-md .cm-scroller::-webkit-scrollbar {
40-
width: 0.4rem;
63+
.cm-editor .cm-scroller {
64+
font-family: inherit;
4165
}
4266

43-
.scrollbar--horizontal-md .cm-scroller::-webkit-scrollbar {
44-
height: 0.4rem;
67+
.cm-editor .cm-scroller::-webkit-scrollbar,
68+
.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul::-webkit-scrollbar {
69+
height: var(--scrollbar-size);
70+
width: var(--scrollbar-size);
4571
}
4672

47-
.cm-editor .cm-scroller::-webkit-scrollbar-track {
48-
background-color: transparent;
73+
.cm-editor .cm-scroller::-webkit-scrollbar-track,
74+
.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul::-webkit-scrollbar-track {
75+
background: transparent;
4976
}
5077

51-
.cm-editor .cm-scroller::-webkit-scrollbar-thumb {
78+
.cm-editor .cm-scroller::-webkit-scrollbar-thumb,
79+
.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul::-webkit-scrollbar-thumb {
5280
background: var(--color-brand);
53-
border-radius: 1rem;
81+
border-radius: var(--scrollbar-radius);
5482
}
5583

56-
.cm-tooltip {
84+
.cm-editor .cm-tooltip {
5785
border: none !important;
5886
outline: none !important;
5987
background: var(--color-theme) !important;

web/client/src/library/components/editor/EditorFooter.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { getLanguageByExtension, showIndicatorDialects } from './help'
22
import EditorIndicator from './EditorIndicator'
33
import { type EditorTab, useStoreEditor } from '~/context/editor'
44
import { useEffect } from 'react'
5-
import { isFalse, isNil, isNotNil, isStringEmptyOrNil } from '~/utils'
6-
import { EnumFileExtensions } from '@models/file'
5+
import { isNil, isNotNil, isStringEmptyOrNil } from '~/utils'
76
import Input from '@components/input/Input'
87
import { EnumSize } from '~/types/enum'
98

@@ -33,23 +32,6 @@ export default function EditorFooter({ tab }: { tab: EditorTab }): JSX.Element {
3332

3433
return (
3534
<div className="flex w-full mr-4 overflow-hidden items-center">
36-
{tab.file.isSQLMeshModelSQL && (
37-
<EditorIndicator
38-
className="mr-2"
39-
text="Valid"
40-
>
41-
<EditorIndicator.Light ok={tab.isValid} />
42-
</EditorIndicator>
43-
)}
44-
{tab.file.extension === EnumFileExtensions.SQL &&
45-
isFalse(tab.file.isSQLMeshModelSQL) && (
46-
<EditorIndicator
47-
className="mr-2"
48-
text="Valid SQL"
49-
>
50-
<EditorIndicator.Light ok={tab.isValid} />
51-
</EditorIndicator>
52-
)}
5335
{tab.file.isRemote && (
5436
<EditorIndicator
5537
className="mr-2"

web/client/src/library/components/editor/extensions/SQLMeshDialect.ts

Lines changed: 92 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
import { keywordCompletionSource, SQLDialect } from '@codemirror/lang-sql'
77
import { LanguageSupport } from '@codemirror/language'
88
import { type Model } from '~/api/client'
9-
import { isFalse, isNil } from '~/utils'
9+
import { isArrayEmpty, isNil, isNotNil, isStringEmptyOrNil } from '~/utils'
1010
import { sqlglotWorker } from '~/workers'
1111

1212
const cache = new Map<string, (e: MessageEvent) => void>()
@@ -30,18 +30,24 @@ export const SQLMeshDialect: ExtensionSQLMeshDialect = function SQLMeshDialect(
3030
'columns grain grains references metric tags audit model name kind owner cron start storage_format time_column partitioned_by pre post batch_size audits dialect'
3131
const SQLMeshTypes =
3232
'expression seed full incremental_by_time_range incremental_by_unique_key view embedded'
33-
3433
const lang = SQLDialect.define({
3534
keywords: (SQLKeywords + WHITE_SPACE + SQLMeshKeywords).toLowerCase(),
3635
types: (SQLTypes + WHITE_SPACE + SQLMeshTypes).toLowerCase(),
3736
})
38-
39-
const tables: Completion[] = Array.from(new Set(Object.values(models))).map(
40-
label => ({
41-
label,
42-
type: 'keyword',
43-
}),
37+
const allModels = Array.from(new Set(models.values()))
38+
const modelColumns = Array.from(
39+
new Set(
40+
allModels.map(model => model.columns.map(column => column.name)).flat(),
41+
),
4442
)
43+
const modelNames: Completion[] = allModels.map(model => ({
44+
label: model.name,
45+
type: 'model',
46+
}))
47+
const columnNames: Completion[] = modelColumns.map(name => ({
48+
label: name,
49+
type: 'column',
50+
}))
4551

4652
SQLMeshDialectCleanUp()
4753

@@ -57,24 +63,45 @@ export const SQLMeshDialect: ExtensionSQLMeshDialect = function SQLMeshDialect(
5763

5864
return new LanguageSupport(lang.language, [
5965
lang.language.data.of({
60-
autocomplete: completeFromList([
61-
{
62-
label: 'model',
63-
type: 'keyword',
64-
apply: 'MODEL (\n\r);',
65-
},
66-
]),
67-
}),
68-
lang.language.data.of({
69-
async autocomplete(ctx: CompletionContext) {
70-
const match = ctx.matchBefore(/\w*$/)?.text.trim() ?? ''
66+
autocomplete(ctx: CompletionContext) {
67+
const dot = ctx.matchBefore(/[A-Za-z0-9_.]*\.(\w+)?\s*$/i)
68+
69+
if (isNotNil(dot)) {
70+
let options = columnNames
71+
const blocks = dot.text.split('.')
72+
const text = blocks.pop()
73+
const maybeModelName = blocks.join('.')
74+
const maybeModel = models.get(maybeModelName)
75+
76+
if (isNotNil(maybeModel)) {
77+
options = maybeModel.columns.map(column => ({
78+
label: column.name,
79+
type: 'column',
80+
}))
81+
}
82+
83+
return {
84+
from: isStringEmptyOrNil(text) ? dot.to : dot.to - text.length,
85+
to: dot.to,
86+
options,
87+
}
88+
}
89+
90+
const word = ctx.matchBefore(/\w*$/)
91+
92+
if (isNil(word) || (word?.from === word?.to && !ctx.explicit)) return
93+
94+
const keywordKind = matchWordWithSpacesAfter(ctx, 'kind')
95+
const keywordDialect = matchWordWithSpacesAfter(ctx, 'dialect')
96+
const keywordModel = matchWordWithSpacesAfter(ctx, 'model')
97+
const keywordFrom = matchWordWithSpacesAfter(ctx, 'from')
98+
const keywordJoin = matchWordWithSpacesAfter(ctx, 'join')
99+
const keywordSelect = matchWordWithSpacesAfter(ctx, 'select')
100+
71101
const text = ctx.state.doc.toJSON().join('\n')
72-
const keywordFrom = ctx.matchBefore(/from.+/i)
73-
const keywordKind = ctx.matchBefore(/kind.+/i)
74-
const keywordDialect = ctx.matchBefore(/dialect.+/i)
75102
const matchModels = text.match(/MODEL \(([\s\S]+?)\);/g) ?? []
76-
const isInsideModel = matchModels
77-
.filter(str => str.includes(match))
103+
const isInModel = matchModels
104+
.filter(str => str.includes(word.text))
78105
.map<[number, number]>(str => [
79106
text.indexOf(str),
80107
text.indexOf(str) + str.length,
@@ -84,26 +111,37 @@ export const SQLMeshDialect: ExtensionSQLMeshDialect = function SQLMeshDialect(
84111
ctx.pos >= start && ctx.pos <= end,
85112
)
86113

87-
let suggestions: Completion[] = tables
88-
89-
if (isFalse(isInsideModel)) {
90-
if (keywordFrom != null)
91-
return await completeFromList(suggestions)(ctx)
92-
93-
return await keywordCompletionSource(lang)(ctx)
94-
}
95-
96-
suggestions = SQLMeshModelDictionary.get('keywords') ?? []
97-
98-
if (keywordKind != null) {
99-
suggestions = SQLMeshModelDictionary.get('kind') ?? []
100-
}
101-
102-
if (keywordDialect != null) {
103-
suggestions = SQLMeshModelDictionary.get('dialect') ?? []
114+
if (isInModel) {
115+
let suggestions = SQLMeshModelDictionary.get('keywords')
116+
117+
if (isNotNil(keywordKind)) {
118+
suggestions = SQLMeshModelDictionary.get('kind')
119+
} else if (isNotNil(keywordDialect)) {
120+
suggestions = SQLMeshModelDictionary.get('dialect')
121+
}
122+
123+
return completeFromList(suggestions ?? [])(ctx)
124+
} else {
125+
let suggestions: Completion[] = []
126+
127+
if (isNotNil(keywordModel) && isArrayEmpty(matchModels)) {
128+
suggestions = [
129+
{
130+
label: 'model',
131+
type: 'keyword',
132+
apply: 'MODEL (\n\r);',
133+
},
134+
]
135+
} else if (isNotNil(keywordSelect)) {
136+
suggestions = columnNames
137+
} else if (isNotNil(keywordFrom) || isNotNil(keywordJoin)) {
138+
suggestions = modelNames
139+
}
140+
141+
return isArrayEmpty(suggestions)
142+
? keywordCompletionSource(lang)(ctx)
143+
: completeFromList(suggestions)(ctx)
104144
}
105-
106-
return await completeFromList(suggestions)(ctx)
107145
},
108146
}),
109147
])
@@ -192,3 +230,14 @@ export function getSQLMeshModelKeywords(
192230
],
193231
])
194232
}
233+
234+
function matchWordWithSpacesAfter(
235+
ctx: CompletionContext,
236+
word: string,
237+
): Maybe<{
238+
from: number
239+
to: number
240+
text: string
241+
}> {
242+
return ctx.matchBefore(new RegExp(`${word}\\s*(?=\\S)([^ ]+)`, 'i'))
243+
}

web/client/src/library/components/graph/Graph.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ const ModelColumnDisplay = memo(function ModelColumnDisplay({
153153
<CodeEditorDefault
154154
content={source}
155155
type={EnumFileExtensions.SQL}
156-
className="scrollbar--vertical-md scrollbar--horizontal-md overflow-auto !max-w-[30rem] !h-[25vh] text-xs"
156+
className="scrollbar--vertical scrollbar--horizontal overflow-auto !max-w-[30rem] !h-[25vh] text-xs"
157157
extensions={modelExtensions}
158158
/>
159159
</Popover.Panel>

0 commit comments

Comments
 (0)