|
| 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