From 82cac0b12e61c0af7ca9603b32bb4938ceb163a8 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 3 Jun 2026 19:52:32 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sql-editor):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AF=B9=E8=B1=A1=E5=85=83=E6=95=B0=E6=8D=AE=E4=B8=8E?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E4=BA=A4=E4=BA=92=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 SQL 元数据 hover provider 多实例重复注册导致内容重复显示 - 修复侧栏对象拖入编辑器后 Monaco 原生拖拽虚线残留 - 修复跨库拖拽对象丢失来源库导致后续无法跳转 - 兼容 macOS Cmd 点击时 Monaco 未提供 leftButton 的事件结构 - 补充 hover 去重、拖拽插入、对象跳转和 Cmd 点击回归测试 --- .../QueryEditor.external-sql-save.test.tsx | 255 ++++++++++++++++++ frontend/src/components/QueryEditor.tsx | 173 ++++++++---- frontend/src/utils/sidebarSqlDrag.ts | 7 + 3 files changed, 380 insertions(+), 55 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index a38f955..e1bbb32 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -675,6 +675,7 @@ describe('QueryEditor external SQL save', () => { connectionId: 'conn-1', dbName: 'analytics', tableName: 'events', + objectType: 'table', }); expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ type: 'gonavi:locate-sidebar-object', @@ -683,6 +684,48 @@ describe('QueryEditor external SQL save', () => { expect(stopPropagation).toHaveBeenCalled(); }); + it('opens a table tab on macOS cmd click when Monaco omits leftButton', async () => { + editorState.value = 'select * from fs_mkefu_regist_record;'; + autoFetchState.visible = true; + backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'mkefu_location_dev_local' }] }); + backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_mkefu_location_dev_local: 'fs_mkefu_regist_record' }] }); + backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] }); + + await act(async () => { + create(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const preventDefault = vi.fn(); + const stopPropagation = vi.fn(); + await act(async () => { + editorState.mouseDownListeners[0]?.({ + target: { position: { lineNumber: 1, column: 'select * from fs_mkefu_regist_record'.length } }, + event: { + browserEvent: { button: 0, buttons: 1 }, + ctrlKey: false, + metaKey: true, + preventDefault, + stopPropagation, + }, + }); + }); + + expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'mkefu_location_dev_local' }); + expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ + type: 'table', + connectionId: 'conn-1', + dbName: 'mkefu_location_dev_local', + tableName: 'fs_mkefu_regist_record', + objectType: 'table', + })); + expect(preventDefault).toHaveBeenCalled(); + expect(stopPropagation).toHaveBeenCalled(); + }); + it('shows link-style hover feedback when ctrl/cmd is pressed over a navigable identifier', async () => { editorState.value = 'select * from analytics.events where id = 1'; autoFetchState.visible = true; @@ -912,6 +955,39 @@ describe('QueryEditor external SQL save', () => { expect(hover?.contents?.[0]?.value).toContain('表:`users`'); }); + it('registers SQL metadata hover provider only once across query editor instances', async () => { + editorState.value = 'select * from H2.S_BUSI'; + autoFetchState.visible = true; + backendApp.DBGetDatabases.mockResolvedValue({ success: true, data: [{ Database: 'H2' }] }); + backendApp.DBGetTables.mockResolvedValue({ success: true, data: [{ Tables_in_H2: 'H2.S_BUSI' }] }); + backendApp.DBGetAllColumns.mockResolvedValue({ success: true, data: [] }); + + let firstRenderer: ReactTestRenderer; + let secondRenderer: ReactTestRenderer; + await act(async () => { + firstRenderer = create(); + }); + await act(async () => { + secondRenderer = create(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(editorState.hoverProviders).toHaveLength(1); + const hover = editorState.hoverProviders[0].provideHover( + editorState.editor.getModel(), + { lineNumber: 1, column: 18 }, + ); + const hoverText = String(hover?.contents?.[0]?.value || ''); + expect(hoverText.match(/\*\*表\*\*/g)).toHaveLength(1); + expect(hoverText).toContain('`H2.S_BUSI`'); + + firstRenderer!.unmount(); + secondRenderer!.unmount(); + }); + it('keeps hover underline active when ctrl/cmd is pressed repeatedly without moving the mouse', async () => { const windowListeners: Record void)[]> = {}; vi.stubGlobal('window', { @@ -2550,6 +2626,7 @@ describe('QueryEditor external SQL save', () => { preventDefault: vi.fn(), stopPropagation: vi.fn(), dataTransfer: { + types: ['application/x-gonavi-sql-object', 'text/plain'], getData: (type: string) => { if (type === 'application/x-gonavi-sql-object') { return JSON.stringify({ text: 'reporting.active_users' }); @@ -2570,6 +2647,184 @@ describe('QueryEditor external SQL save', () => { expect(editorState.value).toContain('reporting.active_users'); }); + it('prevents Monaco native drag marker and keeps metadata hover after sidebar object drops', async () => { + const domListeners: Record void)[]> = {}; + editorState.domNode = { + style: { cursor: '' }, + addEventListener: vi.fn((type: string, listener: (event?: any) => void) => { + domListeners[type] ||= []; + domListeners[type].push(listener); + }), + removeEventListener: vi.fn(), + } as any; + editorState.editor.getTargetAtClientPoint = vi.fn(() => ({ + position: { lineNumber: 1, column: 'SELECT * FROM '.length + 1 }, + })); + editorState.value = 'SELECT * FROM '; + autoFetchState.visible = true; + backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'front_end_sys' }] }); + backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_front_end_sys: 'fs_mkefu_regist_record' }] }); + backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] }); + + await act(async () => { + create(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const dragOverEvent = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + dataTransfer: { + types: ['application/x-gonavi-sql-object', 'text/plain'], + dropEffect: 'none', + getData: vi.fn(() => ''), + }, + }; + await act(async () => { + domListeners.dragover?.forEach((listener) => listener(dragOverEvent)); + }); + + expect(dragOverEvent.preventDefault).toHaveBeenCalled(); + expect(dragOverEvent.stopPropagation).toHaveBeenCalled(); + expect(dragOverEvent.dataTransfer.dropEffect).toBe('copy'); + expect(dragOverEvent.dataTransfer.getData).not.toHaveBeenCalled(); + + await act(async () => { + domListeners.drop?.forEach((listener) => listener({ + clientX: 10, + clientY: 10, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + dataTransfer: { + types: ['application/x-gonavi-sql-object', 'text/plain'], + getData: (type: string) => { + if (type === 'application/x-gonavi-sql-object') { + return JSON.stringify({ text: 'fs_mkefu_regist_record' }); + } + if (type === 'text/plain') { + return 'fs_mkefu_regist_record'; + } + return ''; + }, + }, + })); + }); + + const hover = editorState.hoverProviders[0]?.provideHover( + editorState.editor.getModel(), + { lineNumber: 1, column: 'SELECT * FROM fs_mkefu_regist_record'.length }, + ); + expect(editorState.value).toContain('fs_mkefu_regist_record'); + expect(hover?.contents?.[0]?.value).toContain('**表** `fs_mkefu_regist_record`'); + + await act(async () => { + editorState.mouseDownListeners[0]?.({ + target: { position: { lineNumber: 1, column: 'SELECT * FROM fs_mkefu_regist_record'.length } }, + event: { + leftButton: true, + ctrlKey: true, + metaKey: false, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + }, + }); + }); + + expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'front_end_sys' }); + expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ + type: 'table', + connectionId: 'conn-1', + dbName: 'front_end_sys', + tableName: 'fs_mkefu_regist_record', + objectType: 'table', + })); + }); + + it('keeps sidebar object navigation tied to the dragged database after drop', async () => { + const domListeners: Record void)[]> = {}; + editorState.domNode = { + style: { cursor: '' }, + addEventListener: vi.fn((type: string, listener: (event?: any) => void) => { + domListeners[type] ||= []; + domListeners[type].push(listener); + }), + removeEventListener: vi.fn(), + } as any; + editorState.editor.getTargetAtClientPoint = vi.fn(() => ({ + position: { lineNumber: 1, column: 'SELECT * FROM '.length + 1 }, + })); + editorState.value = 'SELECT * FROM '; + autoFetchState.visible = true; + backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }, { Database: 'front_end_sys' }] }); + backendApp.DBGetTables + .mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] }) + .mockResolvedValueOnce({ success: true, data: [{ Tables_in_front_end_sys: 'fs_mkefu_regist_record' }] }); + backendApp.DBGetAllColumns + .mockResolvedValueOnce({ success: true, data: [] }) + .mockResolvedValueOnce({ success: true, data: [] }); + + await act(async () => { + create(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await act(async () => { + domListeners.drop?.forEach((listener) => listener({ + clientX: 10, + clientY: 10, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + dataTransfer: { + types: ['application/x-gonavi-sql-object', 'text/plain'], + getData: (type: string) => { + if (type === 'application/x-gonavi-sql-object') { + return JSON.stringify({ + text: 'fs_mkefu_regist_record', + nodeType: 'table', + connectionId: 'conn-1', + dbName: 'front_end_sys', + }); + } + if (type === 'text/plain') { + return 'fs_mkefu_regist_record'; + } + return ''; + }, + }, + })); + }); + + expect(editorState.value).toContain('front_end_sys.fs_mkefu_regist_record'); + + await act(async () => { + editorState.mouseDownListeners[0]?.({ + target: { position: { lineNumber: 1, column: 'SELECT * FROM front_end_sys.fs_mkefu_regist_record'.length } }, + event: { + leftButton: true, + ctrlKey: true, + metaKey: false, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + }, + }); + }); + + expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'front_end_sys' }); + expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ + type: 'table', + connectionId: 'conn-1', + dbName: 'front_end_sys', + tableName: 'fs_mkefu_regist_record', + objectType: 'table', + })); + }); + it('runs selected SQL before cursor SQL', async () => { backendApp.DBQueryMulti.mockResolvedValueOnce({ success: true, diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index d58337f..3cef7ee 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -21,7 +21,7 @@ import { resolveCurrentSqlStatementRange, resolveExecutableSql } from '../utils/ import { isMacLikePlatform } from '../utils/appearance'; import { splitSidebarQualifiedName } from '../utils/sidebarLocate'; import { normalizeSidebarViewName } from '../utils/sidebarMetadata'; -import { SIDEBAR_SQL_EDITOR_DRAG_MIME, decodeSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag'; +import { SIDEBAR_SQL_EDITOR_DRAG_MIME, decodeSidebarSqlEditorDragPayload, hasSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag'; import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert'; import { ORACLE_ROWID_LOCATOR_COLUMN, type EditRowLocator } from '../utils/rowLocator'; import { getQueryTabDraft, hasQueryTabDraft, setQueryTabDraft, setSQLFileTabDraft } from '../utils/sqlFileTabDrafts'; @@ -188,9 +188,9 @@ const SQL_FUNCTIONS: { name: string; detail: string }[] = [ { name: 'SLEEP', detail: '工具 - 延时' }, ]; -// HMR 重载时释放旧注册避免补全项重复 +// HMR 重载时释放旧注册避免补全和 hover 内容重复 const _g = globalThis as any; -const SQL_COMPLETION_PROVIDER_VERSION = '20260602-table-fuzzy-lazy-v2'; +const SQL_COMPLETION_PROVIDER_VERSION = '20260603-hover-singleton-v1'; if (!_g.__gonaviSqlCompletionState) { _g.__gonaviSqlCompletionState = { registered: false, version: '', disposables: [] as any[] }; } @@ -213,6 +213,10 @@ type CompletionRoutineMeta = {dbName: string, routineName: string, routineType: let sharedTablesData: CompletionTableMeta[] = []; let sharedAllColumnsData: CompletionColumnMeta[] = []; let sharedVisibleDbs: string[] = []; +let sharedViewsData: CompletionViewMeta[] = []; +let sharedMaterializedViewsData: CompletionViewMeta[] = []; +let sharedTriggersData: CompletionTriggerMeta[] = []; +let sharedRoutinesData: CompletionRoutineMeta[] = []; let sharedColumnsCacheData: Record = {}; const sharedLazyTablesCache: Record = {}; const sharedLazyTablesInFlight: Record | undefined> = {}; @@ -242,9 +246,60 @@ type QueryStatementPlan = { warning?: string; }; -const readSidebarSqlDropText = (event: DragEvent): string => { +const stripSidebarDropIdentifierQuotes = (part: string): string => { + const text = String(part || '').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 shouldPrefixSidebarDropDatabase = ( + payloadConnectionId: string, + payloadDbName: string, + payloadText: string, + currentConnectionId: string, + currentDb: string, +): boolean => { + const sourceDbName = String(payloadDbName || '').trim(); + if (!sourceDbName) return false; + const normalizedSourceDbName = sourceDbName.toLowerCase(); + if (String(currentDb || '').trim().toLowerCase() === normalizedSourceDbName) return false; + + const sourceConnectionId = String(payloadConnectionId || '').trim(); + const targetConnectionId = String(currentConnectionId || '').trim(); + if (sourceConnectionId && targetConnectionId && sourceConnectionId !== targetConnectionId) return false; + + const parts = String(payloadText || '') + .split('.') + .map(stripSidebarDropIdentifierQuotes) + .filter(Boolean); + return parts[0]?.toLowerCase() !== normalizedSourceDbName; +}; + +const isQueryEditorPrimaryMouseButton = (event: any): boolean => { + if (event?.leftButton === true) return true; + if (event?.leftButton === false) return false; + + const browserEvent = event?.browserEvent || event?.nativeEvent || event; + if (browserEvent?.button === 0) return true; + if (event?.button === 0) return true; + if (browserEvent?.buttons === 1) return true; + if (event?.buttons === 1) return true; + return false; +}; + +const readSidebarSqlDropText = ( + event: DragEvent, + currentConnectionId = '', + currentDb = '', +): string => { const payload = decodeSidebarSqlEditorDragPayload(String(event.dataTransfer?.getData(SIDEBAR_SQL_EDITOR_DRAG_MIME) || '')); if (payload?.text) { + if (shouldPrefixSidebarDropDatabase(payload.connectionId || '', payload.dbName || '', payload.text, currentConnectionId, currentDb)) { + return `${String(payload.dbName || '').trim()}.${payload.text}`; + } return payload.text; } return String(event.dataTransfer?.getData('text/plain') || '').trim(); @@ -1821,7 +1876,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const ctrlMetaPressedRef = useRef(false); const objectDecorationIdsRef = useRef([]); const objectHoverActionRef = useRef(null); - const hoverProviderDisposableRef = useRef(null); const dragRef = useRef<{ startY: number, startHeight: number, currentHeight: number } | null>(null); const pendingEditorHeightRef = useRef(editorHeight); const resizeFrameRef = useRef(null); @@ -1966,6 +2020,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc sharedTablesData = tablesRef.current; sharedAllColumnsData = allColumnsRef.current; sharedVisibleDbs = visibleDbsRef.current; + sharedViewsData = viewsRef.current; + sharedMaterializedViewsData = materializedViewsRef.current; + sharedTriggersData = triggersRef.current; + sharedRoutinesData = routinesRef.current; sharedColumnsCacheData = columnsCacheRef.current; }, [isActive, currentDb, currentConnectionId, connections]); @@ -2130,16 +2188,21 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }, []); const handleSidebarObjectDrop = useCallback((event: DragEvent) => { - const dragText = readSidebarSqlDropText(event); - if (!dragText) { + if (!hasSidebarSqlEditorDragPayload(event.dataTransfer)) { return; } event.preventDefault(); event.stopPropagation(); + const dragText = readSidebarSqlDropText(event, currentConnectionIdRef.current, currentDbRef.current); + if (!dragText) { + return; + } const editor = editorRef.current; const dropTarget = editor?.getTargetAtClientPoint?.(event.clientX, event.clientY); - insertTextIntoEditorAtPosition(dragText, normalizeEditorPosition(dropTarget?.position)); - }, [insertTextIntoEditorAtPosition]); + if (insertTextIntoEditorAtPosition(dragText, normalizeEditorPosition(dropTarget?.position))) { + refreshObjectDecorations(QUERY_EDITOR_LIVE_DECORATION_MAX_TEXT_LENGTH); + } + }, [insertTextIntoEditorAtPosition, refreshObjectDecorations]); const handleSelectCurrentStatement = () => { const editor = editorRef.current; @@ -2439,6 +2502,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc if (isActive) { sharedTablesData = allTables; sharedAllColumnsData = allColumns; + sharedViewsData = allViews; + sharedMaterializedViewsData = allMaterializedViews; + sharedTriggersData = allTriggers; + sharedRoutinesData = allRoutines; } refreshObjectDecorations(); }; @@ -2646,9 +2713,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const editorDomNode = editor.getDomNode?.(); const handleEditorDragOver = (rawEvent: Event) => { const event = rawEvent as DragEvent; - const dragText = readSidebarSqlDropText(event); - if (!dragText) return; + if (!hasSidebarSqlEditorDragPayload(event.dataTransfer)) return; event.preventDefault(); + event.stopPropagation(); if (event.dataTransfer) { event.dataTransfer.dropEffect = 'copy'; } @@ -2660,43 +2727,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc // 应用透明主题(主题由 MonacoEditor 包装组件按需注册) monaco.editor.setTheme(darkMode ? 'transparent-dark' : 'transparent-light'); - hoverProviderDisposableRef.current?.dispose?.(); - hoverProviderDisposableRef.current = monaco.languages.registerHoverProvider('sql', { - provideHover: (model: any, position: any) => { - const normalizedPosition = normalizeEditorPosition(position); - if (!normalizedPosition) { - return null; - } - const lineContent = String(model?.getLineContent?.(normalizedPosition.lineNumber) || ''); - const resolveText = getQueryEditorObjectResolveText(model, lineContent); - const hoverTarget = resolveQueryEditorHoverTarget( - resolveText, - lineContent, - normalizedPosition.column, - currentDbRef.current, - visibleDbsRef.current, - tablesRef.current, - allColumnsRef.current, - viewsRef.current, - materializedViewsRef.current, - triggersRef.current, - routinesRef.current, - ); - if (!hoverTarget) { - return null; - } - return { - range: new monaco.Range( - normalizedPosition.lineNumber, - hoverTarget.range.startColumn, - normalizedPosition.lineNumber, - hoverTarget.range.endColumn, - ), - contents: [{ value: buildQueryEditorHoverMarkdown(hoverTarget) }], - }; - }, - }); - objectHoverActionRef.current?.dispose?.(); const showObjectInfoKeybinding = monaco.KeyMod?.CtrlCmd && monaco.KeyCode?.KeyQ ? [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyQ] @@ -2745,8 +2775,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc window.addEventListener('keydown', syncModifierState); window.addEventListener('keyup', syncModifierState); window.addEventListener('blur', handleWindowBlur); - editorDomNode?.addEventListener('dragover', handleEditorDragOver); - editorDomNode?.addEventListener('drop', handleEditorDrop); + editorDomNode?.addEventListener('dragover', handleEditorDragOver, true); + editorDomNode?.addEventListener('drop', handleEditorDrop, true); editor.onMouseDown?.((event: any) => { const browserEvent = event?.event; @@ -2755,7 +2785,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc if (!browserEvent || !targetPosition) { return; } - if (browserEvent.leftButton !== true) { + if (!isQueryEditorPrimaryMouseButton(browserEvent)) { return; } if (!browserEvent.ctrlKey && !browserEvent.metaKey) { @@ -2919,13 +2949,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc setQueryEditorMouseCursor(editor, ''); objectHoverActionRef.current?.dispose?.(); objectHoverActionRef.current = null; - hoverProviderDisposableRef.current?.dispose?.(); - hoverProviderDisposableRef.current = null; window.removeEventListener('keydown', syncModifierState); window.removeEventListener('keyup', syncModifierState); window.removeEventListener('blur', handleWindowBlur); - editorDomNode?.removeEventListener('dragover', handleEditorDragOver); - editorDomNode?.removeEventListener('drop', handleEditorDrop); + editorDomNode?.removeEventListener('dragover', handleEditorDragOver, true); + editorDomNode?.removeEventListener('drop', handleEditorDrop, true); }); refreshObjectDecorations(); @@ -3025,6 +3053,41 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc _g.__gonaviSqlCompletionState.version = SQL_COMPLETION_PROVIDER_VERSION; sqlCompletionDisposables.forEach((d: any) => d?.dispose?.()); sqlCompletionDisposables.length = 0; + sqlCompletionDisposables.push(monaco.languages.registerHoverProvider('sql', { + provideHover: (model: any, position: any) => { + const normalizedPosition = normalizeEditorPosition(position); + if (!normalizedPosition) { + return null; + } + const lineContent = String(model?.getLineContent?.(normalizedPosition.lineNumber) || ''); + const resolveText = getQueryEditorObjectResolveText(model, lineContent); + const hoverTarget = resolveQueryEditorHoverTarget( + resolveText, + lineContent, + normalizedPosition.column, + sharedCurrentDb, + sharedVisibleDbs, + sharedTablesData, + sharedAllColumnsData, + sharedViewsData, + sharedMaterializedViewsData, + sharedTriggersData, + sharedRoutinesData, + ); + if (!hoverTarget) { + return null; + } + return { + range: new monaco.Range( + normalizedPosition.lineNumber, + hoverTarget.range.startColumn, + normalizedPosition.lineNumber, + hoverTarget.range.endColumn, + ), + contents: [{ value: buildQueryEditorHoverMarkdown(hoverTarget) }], + }; + }, + })); sqlCompletionDisposables.push(monaco.languages.registerCompletionItemProvider('sql', { triggerCharacters: ['.', '_', ...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')], provideCompletionItems: async (model: any, position: any) => { diff --git a/frontend/src/utils/sidebarSqlDrag.ts b/frontend/src/utils/sidebarSqlDrag.ts index 613c7e6..d9d6433 100644 --- a/frontend/src/utils/sidebarSqlDrag.ts +++ b/frontend/src/utils/sidebarSqlDrag.ts @@ -15,6 +15,13 @@ export const encodeSidebarSqlEditorDragPayload = (payload: SidebarSqlEditorDragP dbName: payload.dbName ? String(payload.dbName) : undefined, }); +export const hasSidebarSqlEditorDragPayload = (dataTransfer: Pick | null | undefined): boolean => { + const rawTypes = dataTransfer?.types; + if (!rawTypes) return false; + const types = Array.from(rawTypes as any).map((type) => String(type || '').toLowerCase()); + return types.includes(SIDEBAR_SQL_EDITOR_DRAG_MIME); +}; + export const decodeSidebarSqlEditorDragPayload = (value: string): SidebarSqlEditorDragPayload | null => { try { const parsed = JSON.parse(String(value || '')) as SidebarSqlEditorDragPayload;