Skip to content

Commit 1b04da9

Browse files
rin-yatoinvisal
andauthored
feat: added enum completion (#96)
* feat: added enum completion * chore: remove console.log * chore: added guard clause on enumSchema.length * chore: included package.json * chore: moved replaceAll to top level * chore: improved operator checking logic * feat: handle enum inside the IN (...) --------- Co-authored-by: Visal .In <[email protected]>
1 parent 81e5ac8 commit 1b04da9

File tree

4 files changed

+169
-9
lines changed

4 files changed

+169
-9
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"electron-updater": "^5.3.0",
6969
"mysql2": "^3.5.0",
7070
"node-sql-parser": "^4.7.0",
71+
"query-master-lang-sql": "^1.0.2",
7172
"react": "^18.2.0",
7273
"react-dom": "^18.2.0",
7374
"react-router-dom": "^6.8.1",

src/renderer/components/CodeEditor/SqlCodeEditor.tsx

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,43 @@ import {
66
acceptCompletion,
77
completionStatus,
88
startCompletion,
9+
autocompletion,
10+
CompletionContext,
11+
CompletionResult,
912
} from '@codemirror/autocomplete';
1013
import { defaultKeymap, insertTab } from '@codemirror/commands';
1114
import { keymap } from '@codemirror/view';
12-
import { SQLConfig, sql, MySQL } from '@codemirror/lang-sql';
13-
import { Ref, forwardRef } from 'react';
15+
import { Ref, forwardRef, useCallback } from 'react';
1416
import useCodeEditorTheme from './useCodeEditorTheme';
17+
import type { EnumSchema } from 'renderer/screens/DatabaseScreen/QueryWindow';
18+
import { SyntaxNode } from '@lezer/common';
19+
import {
20+
SQLConfig,
21+
sql,
22+
MySQL,
23+
genericCompletion,
24+
keywordCompletionSource,
25+
schemaCompletionSource,
26+
} from 'query-master-lang-sql';
27+
import handleCustomSqlAutoComplete from './handleCustomSqlAutoComplete';
1528

1629
const SqlCodeEditor = forwardRef(function SqlCodeEditor(
17-
props: ReactCodeMirrorProps & { schema: SQLConfig['schema'] },
30+
props: ReactCodeMirrorProps & {
31+
schema: SQLConfig['schema'];
32+
enumSchema: EnumSchema;
33+
},
1834
ref: Ref<ReactCodeMirrorRef>
1935
) {
20-
const { schema, ...codeMirrorProps } = props;
36+
const { schema, enumSchema, ...codeMirrorProps } = props;
2137
const theme = useCodeEditorTheme();
2238

39+
const enumCompletion = useCallback(
40+
(context: CompletionContext, tree: SyntaxNode): CompletionResult | null => {
41+
return handleCustomSqlAutoComplete(context, tree, enumSchema);
42+
},
43+
[enumSchema]
44+
);
45+
2346
return (
2447
<CodeMirror
2548
ref={ref}
@@ -50,7 +73,13 @@ const SqlCodeEditor = forwardRef(function SqlCodeEditor(
5073
]),
5174
sql({
5275
dialect: MySQL,
53-
schema,
76+
}),
77+
autocompletion({
78+
override: [
79+
keywordCompletionSource(MySQL),
80+
schemaCompletionSource({ schema }),
81+
genericCompletion(enumCompletion),
82+
],
5483
}),
5584
]}
5685
{...codeMirrorProps}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { SyntaxNode } from '@lezer/common';
2+
import { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
3+
import { EnumSchema } from 'renderer/screens/DatabaseScreen/QueryWindow';
4+
5+
function getNodeString(context: CompletionContext, node: SyntaxNode) {
6+
return context.state.doc.sliceString(node.from, node.to);
7+
}
8+
9+
function allowNodeWhenSearchForIdentify(
10+
context: CompletionContext,
11+
node: SyntaxNode
12+
) {
13+
if (node.type.name === 'Operator') return true;
14+
if (node.type.name === 'Keyword') {
15+
return ['IN'].includes(getNodeString(context, node).toUpperCase());
16+
}
17+
return false;
18+
}
19+
20+
function searchForIdentifier(
21+
context: CompletionContext,
22+
node: SyntaxNode
23+
): string | null {
24+
let currentNode = node.prevSibling;
25+
while (currentNode) {
26+
if (['CompositeIdentifier', 'Identifier'].includes(currentNode.type.name)) {
27+
return getNodeString(context, currentNode);
28+
} else if (!allowNodeWhenSearchForIdentify(context, node)) {
29+
return null;
30+
}
31+
32+
currentNode = node.prevSibling;
33+
}
34+
35+
return null;
36+
}
37+
38+
function handleEnumAutoComplete(
39+
context: CompletionContext,
40+
node: SyntaxNode,
41+
enumSchema: EnumSchema
42+
): CompletionResult | null {
43+
let currentNode = node;
44+
45+
if (currentNode.type.name !== 'String') {
46+
return null;
47+
}
48+
49+
// This will handle
50+
// SELECT * FROM tblA WHERE tblA.colA IN (....)
51+
if (currentNode?.parent?.type?.name === 'Parens') {
52+
currentNode = currentNode.parent;
53+
}
54+
55+
if (!currentNode.prevSibling) return null;
56+
57+
// Let search for identifer
58+
const identifier = searchForIdentifier(context, currentNode.prevSibling);
59+
if (!identifier) return null;
60+
61+
const [table, column] = identifier.replaceAll('`', '').split('.');
62+
if (!table) return null;
63+
64+
const enumValues = enumSchema.find((tempEnum) => {
65+
if (column) {
66+
// normally column will be enum
67+
return tempEnum.column === column;
68+
} else {
69+
// when column is a keyword, the node will be counted as 2
70+
// so there will be no column, the enum will be in table variable instead
71+
return tempEnum.column === table;
72+
}
73+
})?.values;
74+
75+
if (!enumValues) return null;
76+
77+
const options: CompletionResult['options'] = enumValues.map((value) => ({
78+
label: value,
79+
displayLabel: value,
80+
type: 'keyword',
81+
}));
82+
83+
return {
84+
from: node.from + 1,
85+
to: node.to - 1,
86+
options,
87+
};
88+
}
89+
90+
export default function handleCustomSqlAutoComplete(
91+
context: CompletionContext,
92+
tree: SyntaxNode,
93+
enumSchema: EnumSchema
94+
): CompletionResult | null {
95+
// dont run if there is no enumSchema
96+
if (enumSchema.length === 0) return null;
97+
98+
return handleEnumAutoComplete(context, tree, enumSchema);
99+
}

src/renderer/screens/DatabaseScreen/QueryWindow.tsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ import { transformResultHeaderUseSchema } from 'libs/TransformResult';
2121
import { SqlStatementResult } from 'libs/SqlRunnerManager';
2222
import { EditorState } from '@codemirror/state';
2323

24+
export type EnumSchema = Array<{
25+
table: string;
26+
column: string;
27+
values: string[];
28+
}>;
29+
2430
interface QueryWindowProps {
2531
initialSql?: string;
2632
initialRun?: boolean;
@@ -34,13 +40,16 @@ export default function QueryWindow({
3440
tabKey,
3541
}: QueryWindowProps) {
3642
const editorRef = useRef<ReactCodeMirrorRef>(null);
37-
const { selectedTab, setTabData, saveWindowTabHistory } = useWindowTab();
43+
44+
const [loading, setLoading] = useState(false);
45+
const [queryKeyCounter, setQueryKeyCounter] = useState(0);
46+
const [result, setResult] = useState<SqlStatementResult[]>([]);
47+
3848
const { runner } = useSqlExecute();
3949
const { showErrorDialog } = useDialog();
40-
const [result, setResult] = useState<SqlStatementResult[]>([]);
41-
const [queryKeyCounter, setQueryKeyCounter] = useState(0);
42-
const [loading, setLoading] = useState(false);
4350
const { schema, currentDatabase } = useSchema();
51+
const { selectedTab, setTabData, saveWindowTabHistory } = useWindowTab();
52+
4453
const codeMirrorSchema = useMemo(() => {
4554
return currentDatabase && schema
4655
? Object.values(schema[currentDatabase].tables).reduce(
@@ -52,6 +61,27 @@ export default function QueryWindow({
5261
)
5362
: {};
5463
}, [schema, currentDatabase]);
64+
65+
const enumSchema = useMemo(() => {
66+
if (!schema || !currentDatabase) return [];
67+
68+
const results: EnumSchema = [];
69+
70+
for (const table of Object.values(schema[currentDatabase].tables)) {
71+
for (const column of Object.values(table.columns)) {
72+
if (column.dataType === 'enum') {
73+
results.push({
74+
table: table.name,
75+
column: column.name,
76+
values: column.enumValues || [],
77+
});
78+
}
79+
}
80+
}
81+
82+
return results;
83+
}, [schema, currentDatabase]);
84+
5585
const [code, setCode] = useState(initialSql || '');
5686

5787
const { handleContextMenu } = useContextMenu(() => {
@@ -237,6 +267,7 @@ export default function QueryWindow({
237267
}}
238268
height="100%"
239269
schema={codeMirrorSchema}
270+
enumSchema={enumSchema}
240271
/>
241272
</div>
242273
</div>

0 commit comments

Comments
 (0)