From 93a3240862dae4966cc86a939dfa71c976dd8351 Mon Sep 17 00:00:00 2001 From: Syngnat <92659908+Syngnat@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:47:45 +0800 Subject: [PATCH] fix(query-editor): fallback ctrl-click for OB Oracle schema tables --- frontend/src/components/MonacoEditor.tsx | 135 +++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/frontend/src/components/MonacoEditor.tsx b/frontend/src/components/MonacoEditor.tsx index ce17b244..7701d8a3 100644 --- a/frontend/src/components/MonacoEditor.tsx +++ b/frontend/src/components/MonacoEditor.tsx @@ -24,6 +24,140 @@ const sameEditorPosition = (left: any, right: any): boolean => ( && Number(left?.column) === Number(right?.column) ); +const stripSqlIdentifierQuotes = (value: string): string => { + const text = String(value || '').trim(); + if (!text) return ''; + if ((text.startsWith('`') && text.endsWith('`')) + || (text.startsWith('"') && text.endsWith('"')) + || (text.startsWith('[') && text.endsWith(']'))) { + return text.slice(1, -1).trim(); + } + return text; +}; + +const splitSqlIdentifierPath = (raw: string): string[] => ( + String(raw || '') + .split('.') + .map(stripSqlIdentifierQuotes) + .map((part) => part.trim()) + .filter(Boolean) +); + +const resolveIdentifierWindowAtColumn = ( + lineContent: string, + column: number, +): { start: number; end: number; text: string } | null => { + const text = String(lineContent || ''); + if (!text) return null; + const isIdentChar = (ch: string) => /[A-Za-z0-9_$`"\[\].]/.test(ch || ''); + let offset = Math.max(0, Math.min(text.length - 1, Number(column || 1) - 2)); + if (!isIdentChar(text[offset] || '')) { + if (offset > 0 && isIdentChar(text[offset - 1] || '')) { + offset -= 1; + } else if (offset + 1 < text.length && isIdentChar(text[offset + 1] || '')) { + offset += 1; + } else { + return null; + } + } + let start = offset; + while (start > 0 && isIdentChar(text[start - 1] || '')) start -= 1; + let end = offset + 1; + while (end < text.length && isIdentChar(text[end] || '')) end += 1; + return start < end ? { start, end, text: text.slice(start, end).trim() } : null; +}; + +const isLikelyTableReferenceIdentifier = ( + lineContent: string, + identifierStart: number, +): boolean => { + const beforeIdentifier = String(lineContent || '').slice(0, Math.max(0, identifierStart)); + return /\b(?:from|join|update|into|delete\s+from|alter\s+table|drop\s+table|truncate\s+table)\s*$/i.test(beforeIdentifier); +}; + +const isOceanBaseOracleConnection = (connection: any): boolean => { + const config = connection?.config || {}; + return String(config.type || '').trim().toLowerCase() === 'oceanbase' + && String(config.oceanBaseProtocol || '').trim().toLowerCase() === 'oracle'; +}; + +const installOceanBaseOracleNavigationFallback = (editor: any) => { + const editorDomNode = editor?.getDomNode?.(); + if (!editorDomNode || editor.__gonaviObOracleNavigationFallbackInstalled) { + return; + } + Object.defineProperty(editor, '__gonaviObOracleNavigationFallbackInstalled', { + value: true, + configurable: true, + }); + + const handleMouseDownCapture = (event: MouseEvent) => { + if (event.button !== 0 || !(event.ctrlKey || event.metaKey) || event.altKey) { + return; + } + + const store = useStore.getState(); + const activeTab = (store.tabs || []).find((tab: any) => tab.id === store.activeTabId); + if (!activeTab || activeTab.type !== 'query') { + return; + } + const connectionId = String(activeTab.connectionId || store.activeContext?.connectionId || '').trim(); + if (!connectionId) { + return; + } + const connection = (store.connections || []).find((item: any) => item.id === connectionId); + if (!isOceanBaseOracleConnection(connection)) { + return; + } + + const target = editor.getTargetAtClientPoint?.(event.clientX, event.clientY); + const position = target?.position; + if (!position) { + return; + } + const model = editor.getModel?.(); + const lineContent = String(model?.getLineContent?.(position.lineNumber) || ''); + const identifier = resolveIdentifierWindowAtColumn(lineContent, position.column); + if (!identifier || !identifier.text.includes('.')) { + return; + } + if (!isLikelyTableReferenceIdentifier(lineContent, identifier.start)) { + return; + } + + const parts = splitSqlIdentifierPath(identifier.text); + if (parts.length !== 2) { + return; + } + const [schemaName, tableName] = parts; + if (!schemaName || !tableName) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation?.(); + store.setActiveContext?.({ connectionId, dbName: schemaName }); + store.addTab?.({ + id: `${connectionId}-${schemaName}-table-${tableName}`, + title: tableName, + type: 'table', + connectionId, + dbName: schemaName, + tableName, + initialViewMode: 'fields', + initialViewModeRequestId: String(Date.now()), + objectType: 'table', + returnToTabId: activeTab.id || undefined, + }); + }; + + editorDomNode.addEventListener('mousedown', handleMouseDownCapture, true); + editor.onDidDispose?.(() => { + editorDomNode.removeEventListener('mousedown', handleMouseDownCapture, true); + }); +}; + const patchQueryEditorAiInlineRightArrowFallback = (editor: any, monaco: any) => { const originalAddCommand = editor?.addCommand?.bind?.(editor); if (!originalAddCommand || !monaco?.KeyCode?.RightArrow) { @@ -154,6 +288,7 @@ const MonacoEditor: React.FC = ({ }, [beforeMount]); const handleMount: OnMount = useCallback((editor, monaco) => { + installOceanBaseOracleNavigationFallback(editor); patchQueryEditorAiInlineRightArrowFallback(editor, monaco); onMount?.(editor, monaco); }, [onMount]);