Skip to content

Commit 626857a

Browse files
committed
feat: implement database-agnostic SQL intellisense
- Add IntellisenseProvider base class with context-aware suggestions - Create DatabaseSchemaCache for efficient metadata caching - Implement SQLContextParser to understand cursor position context - Add ClickHouse-specific intellisense adapter with custom functions/keywords - Integrate intellisense with Monaco Editor in QueryEditor component - Add IPC handlers for fetching connection metadata including database type - Support keyword, table, column, and function suggestions based on context - Cache schema information with configurable timeout (default 5 minutes) This provides context-aware SQL intellisense that adapts to different databases
1 parent 458c918 commit 626857a

File tree

14 files changed

+930
-26
lines changed

14 files changed

+930
-26
lines changed

src/main/database/base.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,14 +289,17 @@ export abstract class BaseDatabaseManager implements DatabaseManagerInterface {
289289
database?: string
290290
): Promise<{ success: boolean; schema?: TableSchema; message: string }>
291291

292-
getConnectionInfo(connectionId: string): { host: string; port: number; database: string } | null {
292+
getConnectionInfo(
293+
connectionId: string
294+
): { host: string; port: number; database: string; type: string } | null {
293295
const connection = this.connections.get(connectionId)
294296
if (!connection) return null
295297

296298
return {
297299
host: connection.config.host,
298300
port: connection.config.port,
299-
database: connection.config.database
301+
database: connection.config.database,
302+
type: connection.config.type
300303
}
301304
}
302305

src/main/database/clickhouse.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,13 +456,16 @@ class ClickHouseManager extends BaseDatabaseManager {
456456
return connection?.isConnected || false
457457
}
458458

459-
getConnectionInfo(connectionId: string): { host: string; port: number; database: string } | null {
459+
getConnectionInfo(
460+
connectionId: string
461+
): { host: string; port: number; database: string; type: string } | null {
460462
const connection = this.connections.get(connectionId)
461463
if (connection) {
462464
return {
463465
host: connection.config.host,
464466
port: connection.config.port,
465-
database: connection.config.database
467+
database: connection.config.database,
468+
type: connection.config.type
466469
}
467470
}
468471
return null

src/main/database/interface.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ export interface DatabaseManagerInterface {
184184
): Promise<{ success: boolean; schema?: TableSchema; message: string }>
185185

186186
// Connection info
187-
getConnectionInfo(connectionId: string): { host: string; port: number; database: string } | null
187+
getConnectionInfo(
188+
connectionId: string
189+
): { host: string; port: number; database: string; type: string } | null
188190
getAllConnections(): string[]
189191
getCapabilities(): DatabaseCapabilities
190192

src/main/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,16 @@ ipcMain.handle('db:getAllConnections', async () => {
381381
}
382382
})
383383

384+
ipcMain.handle('db:getConnectionInfo', async (_, connectionId: string) => {
385+
try {
386+
const info = databaseManager.getConnectionInfo(connectionId)
387+
return { success: true, info }
388+
} catch (error) {
389+
console.error('Error getting connection info:', error)
390+
return { success: false, info: null }
391+
}
392+
})
393+
384394
// IPC handler for AI processing
385395
ipcMain.handle('ai:process', async (_, request) => {
386396
try {

src/preload/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ const api = {
4848
executeBulkOperations: (connectionId: string, operations: any[]) =>
4949
ipcRenderer.invoke('db:executeBulkOperations', connectionId, operations),
5050
getPrimaryKeys: (connectionId: string, table: string, database?: string) =>
51-
ipcRenderer.invoke('db:getPrimaryKeys', connectionId, table, database)
51+
ipcRenderer.invoke('db:getPrimaryKeys', connectionId, table, database),
52+
getConnectionInfo: (connectionId: string) =>
53+
ipcRenderer.invoke('db:getConnectionInfo', connectionId)
5254
},
5355
connections: {
5456
getAll: () => ipcRenderer.invoke('connections:getAll'),

src/renderer/components/DatabaseExplorer/DatabaseExplorer.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@ import { useState, useEffect } from 'react'
22
import { Box, Flex, ScrollArea, Text, Button } from '@radix-ui/themes'
33
import { Badge, Skeleton } from '../ui'
44
import './DatabaseExplorer.css'
5-
import {
6-
ReloadIcon,
7-
ChevronRightIcon,
8-
ChevronDownIcon,
9-
TableIcon
10-
} from '@radix-ui/react-icons'
5+
import { ReloadIcon, ChevronRightIcon, ChevronDownIcon, TableIcon } from '@radix-ui/react-icons'
116

127
interface DatabaseExplorerProps {
138
connectionId: string
@@ -272,7 +267,7 @@ export function DatabaseExplorer({
272267
className={`database-item ${db.expanded ? 'expanded' : ''}`}
273268
p="2"
274269
>
275-
<Box
270+
<Box
276271
className="expand-icon"
277272
onClick={() => toggleDatabase(db.name)}
278273
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}
@@ -347,7 +342,7 @@ export function DatabaseExplorer({
347342
style={{ cursor: obj.type === 'table' ? 'pointer' : 'default' }}
348343
>
349344
{obj.type === 'table' && (
350-
<Box
345+
<Box
351346
className="expand-icon"
352347
onClick={() => toggleTable(db.name, obj.name)}
353348
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}

src/renderer/components/QueryEditor/QueryEditor.tsx

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { useState, useRef } from 'react'
1+
import { useState, useRef, useEffect } from 'react'
22
import { Button, Flex, Text, Card, Box } from '@radix-ui/themes'
33
import Editor, { Monaco } from '@monaco-editor/react'
44
import { Skeleton } from '../ui'
55
import { useTheme } from '../../hooks/useTheme'
66
import './QueryEditor.css'
77
import { v4 as uuidv4 } from 'uuid'
8+
import { createIntellisenseProvider } from '../../lib/intellisense'
9+
import type { IntellisenseProvider } from '../../lib/intellisense'
810

911
interface QueryEditorProps {
1012
connectionId: string
@@ -87,22 +89,73 @@ export function QueryEditor({ connectionId, connectionName }: QueryEditorProps)
8789
const [selectedText, setSelectedText] = useState('')
8890
const [currentQueryId, setCurrentQueryId] = useState<string | null>(null)
8991
const editorRef = useRef<any>(null)
92+
const intellisenseProviderRef = useRef<IntellisenseProvider | null>(null)
93+
const completionDisposableRef = useRef<any>(null)
94+
const [databaseType, setDatabaseType] = useState<string>('clickhouse')
95+
96+
useEffect(() => {
97+
const fetchDatabaseType = async () => {
98+
try {
99+
const connInfo = await window.api.database.getConnectionInfo(connectionId)
100+
if (connInfo && connInfo.type) {
101+
setDatabaseType(connInfo.type)
102+
}
103+
} catch (error) {
104+
console.error('Error fetching database type:', error)
105+
}
106+
}
107+
fetchDatabaseType()
108+
}, [connectionId])
109+
110+
useEffect(() => {
111+
return () => {
112+
if (completionDisposableRef.current) {
113+
completionDisposableRef.current.dispose()
114+
}
115+
if (intellisenseProviderRef.current) {
116+
intellisenseProviderRef.current.dispose()
117+
}
118+
}
119+
}, [])
90120

91121
const handleEditorDidMount = (editor: any, monaco: Monaco) => {
92122
editorRef.current = editor
93123

94-
// Configure SQL language settings
95-
monaco.languages.registerCompletionItemProvider('sql', {
96-
provideCompletionItems: (model, position) => {
97-
const suggestions = sqlKeywords.map((keyword) => ({
98-
label: keyword,
99-
kind: monaco.languages.CompletionItemKind.Keyword,
100-
insertText: keyword,
101-
documentation: `SQL keyword: ${keyword}`
102-
}))
103-
return { suggestions }
124+
// Initialize intellisense provider
125+
try {
126+
if (completionDisposableRef.current) {
127+
completionDisposableRef.current.dispose()
104128
}
105-
})
129+
130+
intellisenseProviderRef.current = createIntellisenseProvider(monaco, {
131+
connectionId,
132+
databaseType,
133+
currentDatabase: 'default'
134+
})
135+
136+
completionDisposableRef.current = monaco.languages.registerCompletionItemProvider('sql', {
137+
provideCompletionItems: async (model, position) => {
138+
if (!intellisenseProviderRef.current) {
139+
return { suggestions: [] }
140+
}
141+
return await intellisenseProviderRef.current.provideCompletionItems(model, position)
142+
}
143+
})
144+
} catch (error) {
145+
console.error('Error initializing intellisense:', error)
146+
// Fallback to basic keyword completion
147+
completionDisposableRef.current = monaco.languages.registerCompletionItemProvider('sql', {
148+
provideCompletionItems: (model, position) => {
149+
const suggestions = sqlKeywords.map((keyword) => ({
150+
label: keyword,
151+
kind: monaco.languages.CompletionItemKind.Keyword,
152+
insertText: keyword,
153+
documentation: `SQL keyword: ${keyword}`
154+
}))
155+
return { suggestions }
156+
}
157+
})
158+
}
106159

107160
// Add keyboard shortcuts
108161
editor.addAction({

0 commit comments

Comments
 (0)