Skip to content

Commit b4e41ec

Browse files
committed
feat: add sql editor componemt
1 parent 094291a commit b4e41ec

File tree

5 files changed

+718
-8
lines changed

5 files changed

+718
-8
lines changed

src/client/components/CodeEditor/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ export const CodeDiffEditor = loadable(() =>
1111
export const JsonEditor = loadable(() =>
1212
import('./json').then((m) => ({ default: m.JsonEditor }))
1313
);
14+
15+
export const SQLEditor = loadable(() =>
16+
import('./sql').then((m) => ({ default: m.SQLEditor }))
17+
);
18+
19+
export type { SQLTableSchema } from './sql';
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import type { Monaco } from '@monaco-editor/react';
2+
import type { SQLTableSchema } from '../sql';
3+
4+
/**
5+
* SQL Keywords for autocompletion
6+
*/
7+
export const SQL_KEYWORDS = [
8+
'SELECT',
9+
'FROM',
10+
'WHERE',
11+
'INSERT',
12+
'UPDATE',
13+
'DELETE',
14+
'CREATE',
15+
'DROP',
16+
'ALTER',
17+
'TABLE',
18+
'INDEX',
19+
'VIEW',
20+
'JOIN',
21+
'LEFT JOIN',
22+
'RIGHT JOIN',
23+
'INNER JOIN',
24+
'OUTER JOIN',
25+
'ON',
26+
'AS',
27+
'AND',
28+
'OR',
29+
'NOT',
30+
'IN',
31+
'LIKE',
32+
'BETWEEN',
33+
'ORDER BY',
34+
'GROUP BY',
35+
'HAVING',
36+
'LIMIT',
37+
'OFFSET',
38+
'COUNT',
39+
'SUM',
40+
'AVG',
41+
'MIN',
42+
'MAX',
43+
'DISTINCT',
44+
'NULL',
45+
'IS NULL',
46+
'IS NOT NULL',
47+
'EXISTS',
48+
'CASE',
49+
'WHEN',
50+
'THEN',
51+
'ELSE',
52+
'END',
53+
] as const;
54+
55+
/**
56+
* SQL Functions for autocompletion
57+
*/
58+
export const SQL_FUNCTIONS = [
59+
'COUNT()',
60+
'SUM()',
61+
'AVG()',
62+
'MIN()',
63+
'MAX()',
64+
'UPPER()',
65+
'LOWER()',
66+
'LENGTH()',
67+
'TRIM()',
68+
'CONCAT()',
69+
'SUBSTRING()',
70+
'COALESCE()',
71+
'CAST()',
72+
'CONVERT()',
73+
'NOW()',
74+
'CURRENT_TIMESTAMP',
75+
'CURRENT_DATE',
76+
'CURRENT_TIME',
77+
'DATE()',
78+
'YEAR()',
79+
'MONTH()',
80+
'DAY()',
81+
'HOUR()',
82+
'MINUTE()',
83+
] as const;
84+
85+
/**
86+
* Create SQL Completion Provider
87+
* Provides intelligent autocompletion for SQL queries
88+
*/
89+
export function createSQLCompletionProvider(
90+
monaco: Monaco,
91+
tables: SQLTableSchema[]
92+
) {
93+
return monaco.languages.registerCompletionItemProvider('sql', {
94+
triggerCharacters: ['.', ' '],
95+
provideCompletionItems: (model, position) => {
96+
const textUntilPosition = model.getValueInRange({
97+
startLineNumber: position.lineNumber,
98+
startColumn: 1,
99+
endLineNumber: position.lineNumber,
100+
endColumn: position.column,
101+
});
102+
103+
const word = model.getWordUntilPosition(position);
104+
const range = {
105+
startLineNumber: position.lineNumber,
106+
endLineNumber: position.lineNumber,
107+
startColumn: word.startColumn,
108+
endColumn: word.endColumn,
109+
};
110+
111+
const suggestions: any[] = [];
112+
113+
// Check if we're after a dot (for column suggestions)
114+
const lastDotIndex = textUntilPosition.lastIndexOf('.');
115+
if (
116+
lastDotIndex !== -1 &&
117+
lastDotIndex === textUntilPosition.length - 1
118+
) {
119+
// Get the table name before the dot
120+
const beforeDot = textUntilPosition.substring(0, lastDotIndex).trim();
121+
const tableNameMatch = beforeDot.match(/(\w+)$/);
122+
123+
if (tableNameMatch) {
124+
const tableName = tableNameMatch[1];
125+
const table = tables.find(
126+
(t) => t.name.toLowerCase() === tableName.toLowerCase()
127+
);
128+
129+
if (table) {
130+
// Suggest columns for this table
131+
table.columns.forEach((column) => {
132+
suggestions.push({
133+
label: column.name,
134+
kind: monaco.languages.CompletionItemKind.Field,
135+
detail: column.type,
136+
documentation:
137+
column.description ||
138+
`Column: ${column.name} (${column.type})`,
139+
insertText: column.name,
140+
range: range,
141+
});
142+
});
143+
}
144+
}
145+
} else {
146+
// SQL Keywords
147+
SQL_KEYWORDS.forEach((keyword) => {
148+
suggestions.push({
149+
label: keyword,
150+
kind: monaco.languages.CompletionItemKind.Keyword,
151+
detail: 'SQL Keyword',
152+
insertText: keyword,
153+
range: range,
154+
});
155+
});
156+
157+
// SQL Functions
158+
SQL_FUNCTIONS.forEach((func) => {
159+
suggestions.push({
160+
label: func,
161+
kind: monaco.languages.CompletionItemKind.Function,
162+
detail: 'SQL Function',
163+
insertText: func,
164+
range: range,
165+
});
166+
});
167+
168+
// Table names
169+
tables.forEach((table) => {
170+
suggestions.push({
171+
label: table.name,
172+
kind: monaco.languages.CompletionItemKind.Class,
173+
detail: 'Table',
174+
documentation: `Table with ${table.columns.length} columns`,
175+
insertText: table.name,
176+
range: range,
177+
});
178+
});
179+
180+
// All columns from all tables (when not after a dot)
181+
tables.forEach((table) => {
182+
table.columns.forEach((column) => {
183+
suggestions.push({
184+
label: `${table.name}.${column.name}`,
185+
kind: monaco.languages.CompletionItemKind.Field,
186+
detail: `${column.type} (from ${table.name})`,
187+
documentation:
188+
column.description || `Column: ${column.name} (${column.type})`,
189+
insertText: `${table.name}.${column.name}`,
190+
range: range,
191+
});
192+
});
193+
});
194+
}
195+
196+
return { suggestions };
197+
},
198+
});
199+
}
200+
201+
/**
202+
* Create SQL Hover Provider
203+
* Provides hover information for tables and columns
204+
*/
205+
export function createSQLHoverProvider(
206+
monaco: Monaco,
207+
tables: SQLTableSchema[]
208+
) {
209+
return monaco.languages.registerHoverProvider('sql', {
210+
provideHover: (model, position) => {
211+
const word = model.getWordAtPosition(position);
212+
if (!word) {
213+
return null;
214+
}
215+
216+
const wordText = word.word.toLowerCase();
217+
218+
// Check if it's a table name
219+
const table = tables.find((t) => t.name.toLowerCase() === wordText);
220+
if (table) {
221+
const columnsInfo = table.columns
222+
.map(
223+
(col) =>
224+
`- **${col.name}** (${col.type})${col.description ? `: ${col.description}` : ''}`
225+
)
226+
.join('\n');
227+
228+
return {
229+
contents: [
230+
{ value: `**Table: ${table.name}**` },
231+
{ value: `\nColumns:\n${columnsInfo}` },
232+
],
233+
};
234+
}
235+
236+
// Check if it's a column name
237+
for (const table of tables) {
238+
const column = table.columns.find(
239+
(col) => col.name.toLowerCase() === wordText
240+
);
241+
if (column) {
242+
return {
243+
contents: [
244+
{ value: `**Column: ${column.name}**` },
245+
{ value: `Type: ${column.type}` },
246+
{ value: `Table: ${table.name}` },
247+
...(column.description
248+
? [{ value: `Description: ${column.description}` }]
249+
: []),
250+
],
251+
};
252+
}
253+
}
254+
255+
return null;
256+
},
257+
});
258+
}
259+
260+
/**
261+
* Configure SQL language for Monaco Editor
262+
*/
263+
export function configureSQLLanguage(monaco: Monaco) {
264+
monaco.languages.setLanguageConfiguration('sql', {
265+
comments: {
266+
lineComment: '--',
267+
blockComment: ['/*', '*/'],
268+
},
269+
brackets: [
270+
['(', ')'],
271+
['[', ']'],
272+
],
273+
autoClosingPairs: [
274+
{ open: '(', close: ')' },
275+
{ open: '[', close: ']' },
276+
{ open: "'", close: "'", notIn: ['string', 'comment'] },
277+
{ open: '"', close: '"', notIn: ['string', 'comment'] },
278+
],
279+
});
280+
}

0 commit comments

Comments
 (0)