66import { keywordCompletionSource , SQLDialect } from '@codemirror/lang-sql'
77import { LanguageSupport } from '@codemirror/language'
88import { type Model } from '~/api/client'
9- import { isFalse , isNil } from '~/utils'
9+ import { isArrayEmpty , isNil , isNotNil , isStringEmptyOrNil } from '~/utils'
1010import { sqlglotWorker } from '~/workers'
1111
1212const 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 - Z a - z 0 - 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 ( / f r o m .+ / i)
73- const keywordKind = ctx . matchBefore ( / k i n d .+ / i)
74- const keywordDialect = ctx . matchBefore ( / d i a l e c t .+ / i)
75102 const matchModels = text . match ( / M O D E L \( ( [ \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+ }
0 commit comments