Skip to content

Commit 6f5c924

Browse files
authored
feat: Improve auto complete (#183)
* improve auto complete * escape id if it is conflicted with keywords * feat: improve the auto complete in the middle of the statement
1 parent 7eee130 commit 6f5c924

File tree

5 files changed

+273
-175
lines changed

5 files changed

+273
-175
lines changed

src/renderer/components/CodeEditor/SchemaCompletionTree.ts

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import { Completion } from '@codemirror/autocomplete';
2+
import { SQLDialectSpec } from 'language/dist';
23
import {
34
DatabaseSchema,
45
DatabaseSchemaList,
56
TableSchema,
67
} from 'types/SqlSchema';
78

8-
function buildTableCompletionTree(table: TableSchema): SchemaCompletionTree {
9+
function buildTableCompletionTree(
10+
table: TableSchema,
11+
dialect: SQLDialectSpec,
12+
keywords: Record<string, boolean>,
13+
): SchemaCompletionTree {
914
const root = new SchemaCompletionTree();
1015

1116
for (const col of Object.values(table.columns)) {
1217
root.addOption(col.name, {
1318
label: col.name,
19+
apply: escapeConflictedId(dialect, col.name, keywords),
1420
type: 'property',
1521
detail: col.dataType,
1622
boost: 3,
@@ -21,7 +27,9 @@ function buildTableCompletionTree(table: TableSchema): SchemaCompletionTree {
2127
}
2228

2329
function buildDatabaseCompletionTree(
24-
database: DatabaseSchema
30+
database: DatabaseSchema,
31+
dialect: SQLDialectSpec,
32+
keywords: Record<string, boolean>,
2533
): SchemaCompletionTree {
2634
const root = new SchemaCompletionTree();
2735

@@ -33,15 +41,30 @@ function buildDatabaseCompletionTree(
3341
boost: 1,
3442
});
3543

36-
root.addChild(table.name, buildTableCompletionTree(table));
44+
root.addChild(
45+
table.name,
46+
buildTableCompletionTree(table, dialect, keywords),
47+
);
3748
}
3849

3950
return root;
4051
}
4152

53+
function escapeConflictedId(
54+
dialect: SQLDialectSpec,
55+
label: string,
56+
keywords: Record<string, boolean>,
57+
): string {
58+
if (keywords[label.toLowerCase()])
59+
return `${dialect.identifierQuotes}${label}${dialect.identifierQuotes}`;
60+
return label;
61+
}
62+
4263
function buildCompletionTree(
4364
schema: DatabaseSchemaList | undefined,
44-
currentDatabase: string | undefined
65+
currentDatabase: string | undefined,
66+
dialect: SQLDialectSpec,
67+
keywords: Record<string, boolean>,
4568
): SchemaCompletionTree {
4669
const root: SchemaCompletionTree = new SchemaCompletionTree();
4770
if (!schema) return root;
@@ -51,36 +74,63 @@ function buildCompletionTree(
5174
for (const table of Object.values(schema[currentDatabase].tables)) {
5275
root.addOption(table.name, {
5376
label: table.name,
77+
apply: escapeConflictedId(dialect, table.name, keywords),
5478
type: 'table',
5579
detail: 'table',
5680
boost: 1,
5781
});
5882

59-
root.addChild(table.name, buildTableCompletionTree(table));
83+
root.addChild(
84+
table.name,
85+
buildTableCompletionTree(table, dialect, keywords),
86+
);
6087
}
6188
}
6289

6390
for (const database of Object.values(schema)) {
6491
root.addOption(database.name, {
6592
label: database.name,
93+
apply: escapeConflictedId(dialect, database.name, keywords),
6694
type: 'property',
6795
detail: 'database',
6896
});
6997

70-
root.addChild(database.name, buildDatabaseCompletionTree(database));
98+
root.addChild(
99+
database.name,
100+
buildDatabaseCompletionTree(database, dialect, keywords),
101+
);
71102
}
72103

73104
return root;
74105
}
75106
export default class SchemaCompletionTree {
76107
protected options: Record<string, Completion> = {};
77108
protected child: Record<string, SchemaCompletionTree> = {};
109+
protected keywords: Record<string, boolean> = {};
78110

79111
static build(
80112
schema: DatabaseSchemaList | undefined,
81-
currentDatabase: string | undefined
113+
currentDatabase: string | undefined,
114+
dialect: SQLDialectSpec,
82115
) {
83-
return buildCompletionTree(schema, currentDatabase);
116+
const keywords = (dialect.keywords + ' ' + dialect.builtin)
117+
.split(' ')
118+
.filter(Boolean)
119+
.map((s) => s.toLowerCase());
120+
121+
const keywordDict = keywords.reduce(
122+
(a, keyword) => {
123+
a[keyword] = true;
124+
return a;
125+
},
126+
{} as Record<string, boolean>,
127+
);
128+
129+
return buildCompletionTree(schema, currentDatabase, dialect, keywordDict);
130+
}
131+
132+
getLength() {
133+
return Object.keys(this.options).length;
84134
}
85135

86136
addOption(name: string, complete: Completion) {

src/renderer/components/CodeEditor/SqlCodeEditor.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { MySQLDialect, MySQLTooltips } from 'dialects/MySQLDialect';
3030
import { QueryDialetType } from 'libs/QueryBuilder';
3131
import { PgDialect, PgTooltips } from 'dialects/PgDialect copy';
3232
import { useKeybinding } from 'renderer/contexts/KeyBindingProvider';
33+
import SchemaCompletionTree from './SchemaCompletionTree';
3334

3435
const SqlCodeEditor = forwardRef(function SqlCodeEditor(
3536
props: ReactCodeMirrorProps & {
@@ -43,16 +44,28 @@ const SqlCodeEditor = forwardRef(function SqlCodeEditor(
4344
const { binding } = useKeybinding();
4445
const theme = useCodeEditorTheme();
4546

47+
const dialect = props.dialect === 'mysql' ? MySQLDialect : PgDialect;
48+
const tooltips = props.dialect === 'mysql' ? MySQLTooltips : PgTooltips;
49+
50+
const schemaTree = useMemo(() => {
51+
return SchemaCompletionTree.build(
52+
schema?.getSchema(),
53+
currentDatabase,
54+
dialect.spec,
55+
);
56+
}, [schema, currentDatabase, dialect]);
57+
4658
const customAutoComplete = useCallback(
4759
(context: CompletionContext, tree: SyntaxNode): CompletionResult | null => {
4860
return handleCustomSqlAutoComplete(
4961
context,
5062
tree,
63+
schemaTree,
5164
schema?.getSchema(),
5265
currentDatabase,
5366
);
5467
},
55-
[schema, currentDatabase],
68+
[schema, schemaTree, currentDatabase],
5669
);
5770

5871
const tableNameHighlightPlugin = useMemo(() => {
@@ -64,9 +77,6 @@ const SqlCodeEditor = forwardRef(function SqlCodeEditor(
6477
return createSQLTableNameHighlightPlugin([]);
6578
}, [schema, currentDatabase]);
6679

67-
const dialect = props.dialect === 'mysql' ? MySQLDialect : PgDialect;
68-
const tooltips = props.dialect === 'mysql' ? MySQLTooltips : PgTooltips;
69-
7080
const keyExtension = useMemo(() => {
7181
return keymap.of([
7282
// Prevent the default behavior if it matches any of
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { EditorState } from '@codemirror/state';
2+
import {
3+
CompletionContext,
4+
CompletionResult,
5+
CompletionSource,
6+
} from '@codemirror/autocomplete';
7+
import handleCustomSqlAutoComplete from './handleCustomSqlAutoComplete';
8+
import { MySQL, genericCompletion } from './../../../language/dist';
9+
import {
10+
DatabaseSchemaList,
11+
TableColumnSchema,
12+
TableSchema,
13+
} from 'types/SqlSchema';
14+
import SchemaCompletionTree from './SchemaCompletionTree';
15+
16+
export function get_test_autocomplete(
17+
doc: string,
18+
{
19+
schema,
20+
currentDatabase,
21+
}: { schema: DatabaseSchemaList; currentDatabase?: string },
22+
) {
23+
const cur = doc.indexOf('|'),
24+
dialect = MySQL;
25+
26+
doc = doc.slice(0, cur) + doc.slice(cur + 1);
27+
const state = EditorState.create({
28+
doc,
29+
selection: { anchor: cur },
30+
extensions: [
31+
dialect,
32+
dialect.language.data.of({
33+
autocomplete: genericCompletion((context, tree) =>
34+
handleCustomSqlAutoComplete(
35+
context,
36+
tree,
37+
SchemaCompletionTree.build(schema, currentDatabase, dialect.spec),
38+
schema,
39+
currentDatabase,
40+
),
41+
),
42+
}),
43+
],
44+
});
45+
46+
const result = state.languageDataAt<CompletionSource>('autocomplete', cur)[0](
47+
new CompletionContext(state, cur, false),
48+
);
49+
return result as CompletionResult | null;
50+
}
51+
52+
export function convert_autocomplete_to_string(
53+
result: CompletionResult | null,
54+
) {
55+
return !result
56+
? ''
57+
: result.options
58+
.slice()
59+
.sort(
60+
(a, b) =>
61+
(b.boost || 0) - (a.boost || 0) || (a.label < b.label ? -1 : 1),
62+
)
63+
.map((o) => o.apply || o.label)
64+
.join(', ');
65+
}
66+
67+
function map_column_type(
68+
tableName: string,
69+
name: string,
70+
type: string,
71+
): TableColumnSchema {
72+
const tokens = type.split('(');
73+
let enumValues: string[] | undefined;
74+
75+
if (tokens[1]) {
76+
// remove )
77+
enumValues = tokens[1]
78+
.replace(')', '')
79+
.replaceAll("'", '')
80+
.split(',')
81+
.map((a) => a.trim());
82+
}
83+
84+
return {
85+
name,
86+
tableName,
87+
schemaName: '',
88+
charLength: 0,
89+
comment: '',
90+
enumValues,
91+
dataType: tokens[0],
92+
nullable: true,
93+
};
94+
}
95+
96+
function map_cols(
97+
tableName: string,
98+
cols: Record<string, string>,
99+
): Record<string, TableColumnSchema> {
100+
return Object.entries(cols).reduce(
101+
(acc, [colName, colType]) => {
102+
acc[colName] = map_column_type(tableName, colName, colType);
103+
return acc;
104+
},
105+
{} as Record<string, TableColumnSchema>,
106+
);
107+
}
108+
109+
function map_table(
110+
tables: Record<string, Record<string, string>>,
111+
): Record<string, TableSchema> {
112+
return Object.entries(tables).reduce(
113+
(acc, [tableName, cols]) => {
114+
acc[tableName] = {
115+
columns: map_cols(tableName, cols),
116+
constraints: [],
117+
name: tableName,
118+
type: 'TABLE',
119+
primaryKey: [],
120+
};
121+
122+
return acc;
123+
},
124+
{} as Record<string, TableSchema>,
125+
);
126+
}
127+
128+
export function create_test_schema(
129+
schemas: Record<string, Record<string, Record<string, string>>>,
130+
) {
131+
return Object.entries(schemas).reduce((acc, [schema, tables]) => {
132+
acc[schema] = {
133+
name: schema,
134+
events: [],
135+
triggers: [],
136+
tables: map_table(tables),
137+
};
138+
return acc;
139+
}, {} as DatabaseSchemaList);
140+
}

0 commit comments

Comments
 (0)