🐛 fix(sql-editor): 修复对象元数据与跳转交互异常

- 修复 SQL 元数据 hover provider 多实例重复注册导致内容重复显示
- 修复侧栏对象拖入编辑器后 Monaco 原生拖拽虚线残留
- 修复跨库拖拽对象丢失来源库导致后续无法跳转
- 兼容 macOS Cmd 点击时 Monaco 未提供 leftButton 的事件结构
- 补充 hover 去重、拖拽插入、对象跳转和 Cmd 点击回归测试
This commit is contained in:
Syngnat
2026-06-03 19:52:32 +08:00
parent 1ae44941dd
commit 82cac0b12e
3 changed files with 380 additions and 55 deletions

View File

@@ -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(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'mkefu_location_dev_local' })} />);
});
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(<QueryEditor tab={createTab({ id: 'tab-1', query: editorState.value, dbName: 'H2' })} isActive={false} />);
});
await act(async () => {
secondRenderer = create(<QueryEditor tab={createTab({ id: 'tab-2', query: editorState.value, dbName: 'H2' })} isActive />);
});
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<string, ((event?: any) => 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<string, ((event?: any) => 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(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'front_end_sys' })} />);
});
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<string, ((event?: any) => 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(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'main' })} />);
});
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,

View File

@@ -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<string, any[]> = {};
const sharedLazyTablesCache: Record<string, CompletionTableMeta[] | undefined> = {};
const sharedLazyTablesInFlight: Record<string, Promise<CompletionTableMeta[]> | 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<string[]>([]);
const objectHoverActionRef = useRef<any>(null);
const hoverProviderDisposableRef = useRef<any>(null);
const dragRef = useRef<{ startY: number, startHeight: number, currentHeight: number } | null>(null);
const pendingEditorHeightRef = useRef(editorHeight);
const resizeFrameRef = useRef<number | null>(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) => {

View File

@@ -15,6 +15,13 @@ export const encodeSidebarSqlEditorDragPayload = (payload: SidebarSqlEditorDragP
dbName: payload.dbName ? String(payload.dbName) : undefined,
});
export const hasSidebarSqlEditorDragPayload = (dataTransfer: Pick<DataTransfer, 'types'> | 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;