mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-12 17:39:42 +08:00
🐛 fix(query-editor): 修复连续按 Ctrl/Cmd 时对象跳转失效
This commit is contained in:
@@ -81,6 +81,7 @@ const editorState = vi.hoisted(() => {
|
||||
const state = {
|
||||
value: '',
|
||||
editor: null as any,
|
||||
domNode: { style: { cursor: '' } },
|
||||
position: { lineNumber: 1, column: 1 },
|
||||
selection: null as any,
|
||||
providers: [] as any[],
|
||||
@@ -140,6 +141,7 @@ const editorState = vi.hoisted(() => {
|
||||
state.position = position;
|
||||
}),
|
||||
getSelection: vi.fn(() => state.selection),
|
||||
getDomNode: vi.fn(() => state.domNode),
|
||||
setSelection: vi.fn((selection: any) => {
|
||||
state.selection = selection;
|
||||
}),
|
||||
@@ -348,6 +350,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
editorState.value = '';
|
||||
editorState.position = { lineNumber: 1, column: 1 };
|
||||
editorState.selection = null;
|
||||
editorState.domNode.style.cursor = '';
|
||||
editorState.providers = [];
|
||||
editorState.cursorPositionListeners = [];
|
||||
editorState.mouseMoveListeners = [];
|
||||
@@ -525,16 +528,83 @@ describe('QueryEditor external SQL save', () => {
|
||||
});
|
||||
|
||||
expect(editorState.editor.deltaDecorations).toHaveBeenCalled();
|
||||
expect(editorState.editor.updateOptions).toHaveBeenCalledWith({ mouseStyle: 'pointer' });
|
||||
expect(editorState.domNode.style.cursor).toBe('pointer');
|
||||
const lastDecorationCall = editorState.editor.deltaDecorations.mock.calls.at(-1);
|
||||
expect(lastDecorationCall?.[1]?.[0]?.options?.inlineClassName).toBe('gonavi-query-editor-link-hint');
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseLeaveListeners[0]?.();
|
||||
});
|
||||
expect(editorState.domNode.style.cursor).toBe('');
|
||||
expect(editorState.editor.updateOptions).toHaveBeenLastCalledWith({ mouseStyle: 'text' });
|
||||
});
|
||||
|
||||
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', {
|
||||
addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
|
||||
windowListeners[type] ||= [];
|
||||
windowListeners[type].push(listener);
|
||||
}),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
});
|
||||
|
||||
editorState.value = 'select * from analytics.events where id = 1';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }, { Database: 'analytics' }] });
|
||||
backendApp.DBGetTables
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] })
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_analytics: 'events' }] });
|
||||
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 () => {
|
||||
editorState.mouseMoveListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 27 } },
|
||||
event: {
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const firstDecorationCallCount = editorState.editor.deltaDecorations.mock.calls.length;
|
||||
expect(firstDecorationCallCount).toBeGreaterThan(0);
|
||||
expect(editorState.domNode.style.cursor).toBe('pointer');
|
||||
|
||||
await act(async () => {
|
||||
const repeatedCtrlEvent = {
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
key: 'Control',
|
||||
code: 'ControlLeft',
|
||||
repeat: true,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
target: null,
|
||||
};
|
||||
windowListeners.keydown?.forEach((listener) => listener(repeatedCtrlEvent));
|
||||
windowListeners.keydown?.forEach((listener) => listener(repeatedCtrlEvent));
|
||||
});
|
||||
|
||||
expect(editorState.editor.deltaDecorations.mock.calls.length).toBeGreaterThan(firstDecorationCallCount);
|
||||
expect(editorState.domNode.style.cursor).toBe('pointer');
|
||||
const lastDecorationCall = editorState.editor.deltaDecorations.mock.calls.at(-1);
|
||||
expect(lastDecorationCall?.[1]?.[0]?.options?.inlineClassName).toBe('gonavi-query-editor-link-hint');
|
||||
});
|
||||
|
||||
it('opens a view tab on ctrl left click inside the editor', async () => {
|
||||
editorState.value = 'select * from reporting.active_users';
|
||||
autoFetchState.visible = true;
|
||||
|
||||
@@ -1412,6 +1412,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const selectCurrentStatementActionRef = useRef<any>(null);
|
||||
const lastExternalQueryRef = useRef<string>(getTabQueryValue(tab));
|
||||
const lastEditorCursorPositionRef = useRef<any>(null);
|
||||
const lastHoverTargetPositionRef = useRef<{ lineNumber: number; column: number } | null>(null);
|
||||
const lastExecutedEditorQueryRef = useRef<string>('');
|
||||
const linkDecorationIdsRef = useRef<string[]>([]);
|
||||
const ctrlMetaPressedRef = useRef(false);
|
||||
@@ -1868,16 +1869,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
monacoRef.current = monaco;
|
||||
lastEditorCursorPositionRef.current = normalizeEditorPosition(editor.getPosition?.());
|
||||
|
||||
const applyNavigationHoverState = (event: any) => {
|
||||
const applyNavigationHoverStateAtPosition = (targetPosition: { lineNumber: number; column: number } | null) => {
|
||||
if (!ctrlMetaPressedRef.current) {
|
||||
clearQueryEditorLinkDecorations(editor, linkDecorationIdsRef);
|
||||
editor.updateOptions?.({ mouseStyle: 'text' });
|
||||
setQueryEditorMouseCursor(editor, '');
|
||||
return;
|
||||
}
|
||||
const targetPosition = normalizeEditorPosition(event?.target?.position);
|
||||
if (!targetPosition) {
|
||||
clearQueryEditorLinkDecorations(editor, linkDecorationIdsRef);
|
||||
editor.updateOptions?.({ mouseStyle: 'text' });
|
||||
setQueryEditorMouseCursor(editor, '');
|
||||
return;
|
||||
}
|
||||
const model = editor.getModel?.();
|
||||
@@ -1896,6 +1898,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
if (decorations.length === 0) {
|
||||
clearQueryEditorLinkDecorations(editor, linkDecorationIdsRef);
|
||||
editor.updateOptions?.({ mouseStyle: 'text' });
|
||||
setQueryEditorMouseCursor(editor, '');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1914,20 +1917,36 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
},
|
||||
})),
|
||||
);
|
||||
editor.updateOptions?.({ mouseStyle: 'pointer' });
|
||||
setQueryEditorMouseCursor(editor, 'pointer');
|
||||
};
|
||||
|
||||
const applyNavigationHoverState = (event: any) => {
|
||||
const targetPosition = normalizeEditorPosition(event?.target?.position);
|
||||
lastHoverTargetPositionRef.current = targetPosition;
|
||||
applyNavigationHoverStateAtPosition(targetPosition);
|
||||
};
|
||||
|
||||
const syncModifierState = (keyboardEvent?: KeyboardEvent | MouseEvent | null) => {
|
||||
const wasPressed = ctrlMetaPressedRef.current;
|
||||
ctrlMetaPressedRef.current = !!(keyboardEvent?.ctrlKey || keyboardEvent?.metaKey);
|
||||
if (!ctrlMetaPressedRef.current) {
|
||||
clearQueryEditorLinkDecorations(editor, linkDecorationIdsRef);
|
||||
editor.updateOptions?.({ mouseStyle: 'text' });
|
||||
setQueryEditorMouseCursor(editor, '');
|
||||
return;
|
||||
}
|
||||
const isKeyboardLikeEvent = keyboardEvent
|
||||
&& typeof keyboardEvent === 'object'
|
||||
&& ('key' in keyboardEvent || 'code' in keyboardEvent || 'repeat' in keyboardEvent);
|
||||
if (!wasPressed || isKeyboardLikeEvent) {
|
||||
applyNavigationHoverStateAtPosition(lastHoverTargetPositionRef.current);
|
||||
}
|
||||
};
|
||||
const handleWindowBlur = () => {
|
||||
ctrlMetaPressedRef.current = false;
|
||||
clearQueryEditorLinkDecorations(editor, linkDecorationIdsRef);
|
||||
editor.updateOptions?.({ mouseStyle: 'text' });
|
||||
setQueryEditorMouseCursor(editor, '');
|
||||
};
|
||||
|
||||
// 应用透明主题(主题由 MonacoEditor 包装组件按需注册)
|
||||
@@ -1945,8 +1964,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
applyNavigationHoverState(event);
|
||||
});
|
||||
editor.onMouseLeave?.(() => {
|
||||
lastHoverTargetPositionRef.current = null;
|
||||
clearQueryEditorLinkDecorations(editor, linkDecorationIdsRef);
|
||||
editor.updateOptions?.({ mouseStyle: 'text' });
|
||||
setQueryEditorMouseCursor(editor, '');
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', syncModifierState);
|
||||
@@ -2100,6 +2121,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
|
||||
editor.onDidDispose?.(() => {
|
||||
clearQueryEditorLinkDecorations(editor, linkDecorationIdsRef);
|
||||
setQueryEditorMouseCursor(editor, '');
|
||||
window.removeEventListener('keydown', syncModifierState);
|
||||
window.removeEventListener('keyup', syncModifierState);
|
||||
window.removeEventListener('blur', handleWindowBlur);
|
||||
@@ -4066,7 +4088,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
const setQueryEditorMouseCursor = (
|
||||
editor: any,
|
||||
cursor: '' | 'pointer',
|
||||
) => {
|
||||
const domNode = editor?.getDomNode?.();
|
||||
if (domNode?.style) {
|
||||
domNode.style.cursor = cursor;
|
||||
}
|
||||
};
|
||||
|
||||
export default React.memo(QueryEditor);
|
||||
|
||||
Reference in New Issue
Block a user