mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 18:39:54 +08:00
🐛 fix(sql-editor): 修复对象元数据与跳转交互异常
- 修复 SQL 元数据 hover provider 多实例重复注册导致内容重复显示 - 修复侧栏对象拖入编辑器后 Monaco 原生拖拽虚线残留 - 修复跨库拖拽对象丢失来源库导致后续无法跳转 - 兼容 macOS Cmd 点击时 Monaco 未提供 leftButton 的事件结构 - 补充 hover 去重、拖拽插入、对象跳转和 Cmd 点击回归测试
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user