Merge pull request #765 from virgosgod-maker/fix/issue-753

This commit is contained in:
Syngnat
2026-07-28 19:31:00 +08:00
2 changed files with 108 additions and 0 deletions

View File

@@ -225,6 +225,8 @@ const editorState = vi.hoisted(() => {
decorationIds: [] as string[],
contentHoverCalls: [] as any[],
latestOnChange: null as null | ((value?: string) => void),
transformToUppercaseRun: vi.fn(),
transformToLowercaseRun: vi.fn(),
};
const offsetAt = (position: { lineNumber: number; column: number }) => {
const text = state.value;
@@ -304,6 +306,15 @@ const editorState = vi.hoisted(() => {
state.value = state.value.slice(0, start) + edit.text + state.value.slice(end);
});
}),
getAction: vi.fn((id: string) => {
if (id === 'editor.action.transformToUppercase') {
return { run: state.transformToUppercaseRun };
}
if (id === 'editor.action.transformToLowercase') {
return { run: state.transformToLowercaseRun };
}
return null;
}),
addAction: vi.fn(),
addCommand: vi.fn(),
onDidChangeModelContent: vi.fn((listener: (event?: any) => void) => {
@@ -925,6 +936,9 @@ describe('QueryEditor external SQL save', () => {
editorState.editor.getModel().getValueLength.mockClear();
editorState.editor.setValue.mockClear();
editorState.editor.executeEdits.mockClear();
editorState.editor.getAction.mockClear();
editorState.transformToUppercaseRun.mockReset();
editorState.transformToLowercaseRun.mockReset();
editorState.editor.getScrollLeft.mockClear();
editorState.editor.setScrollLeft.mockClear();
editorState.editor.deltaDecorations.mockClear();
@@ -4824,6 +4838,36 @@ describe('QueryEditor external SQL save', () => {
}));
});
it('registers SQL case conversion context-menu actions and delegates to Monaco', async () => {
await act(async () => {
create(<QueryEditor tab={createTab()} />);
});
const uppercaseAction = findEditorAction('gonavi.queryEditor.transformToUppercase');
const lowercaseAction = findEditorAction('gonavi.queryEditor.transformToLowercase');
expect(uppercaseAction).toMatchObject({
label: '转大写',
precondition: '!editorReadonly',
contextMenuGroupId: '1_modification',
contextMenuOrder: 1,
});
expect(lowercaseAction).toMatchObject({
label: '转小写',
precondition: '!editorReadonly',
contextMenuGroupId: '1_modification',
contextMenuOrder: 2,
});
await uppercaseAction.run(editorState.editor);
await lowercaseAction.run(editorState.editor);
expect(editorState.editor.getAction).toHaveBeenNthCalledWith(1, 'editor.action.transformToUppercase');
expect(editorState.editor.getAction).toHaveBeenNthCalledWith(2, 'editor.action.transformToLowercase');
expect(editorState.transformToUppercaseRun).toHaveBeenCalledOnce();
expect(editorState.transformToLowercaseRun).toHaveBeenCalledOnce();
});
it('localizes Monaco action labels for the active language', async () => {
setCurrentLanguage('en-US');
storeState.shortcutOptions.runQuery.mac = { enabled: true, combo: 'Meta+Q' };
@@ -4846,6 +4890,12 @@ describe('QueryEditor external SQL save', () => {
expect(findEditorAction('gonavi.insertSqlSnippet')).toMatchObject({
label: 'Insert SQL Snippet',
});
expect(findEditorAction('gonavi.queryEditor.transformToUppercase')).toMatchObject({
label: 'convert to uppercase',
});
expect(findEditorAction('gonavi.queryEditor.transformToLowercase')).toMatchObject({
label: 'convert to lowercase',
});
expect(findEditorAction('gonavi.selectCurrentStatement')).toMatchObject({
label: 'GoNavi: Select Current Line and Copy',
});
@@ -4878,6 +4928,12 @@ describe('QueryEditor external SQL save', () => {
expect(findEditorAction('gonavi.insertSqlSnippet')).toMatchObject({
label: '插入 SQL 片段',
});
expect(findEditorAction('gonavi.queryEditor.transformToUppercase')).toMatchObject({
label: '转大写',
});
expect(findEditorAction('gonavi.queryEditor.transformToLowercase')).toMatchObject({
label: '转小写',
});
expect(findEditorAction('gonavi.selectCurrentStatement')).toMatchObject({
label: 'GoNavi: 选择当前行并复制',
});
@@ -4897,6 +4953,8 @@ describe('QueryEditor external SQL save', () => {
expect(findEditorActionLabels('gonavi.queryEditor.showObjectInfo')).toContain('GoNavi: Show Object Info');
expect(findEditorActionLabels('gonavi.runQuery')).toContain('GoNavi: Run SQL');
expect(findEditorActionLabels('gonavi.insertSqlSnippet')).toContain('Insert SQL Snippet');
expect(findEditorActionLabels('gonavi.queryEditor.transformToUppercase')).toContain('convert to uppercase');
expect(findEditorActionLabels('gonavi.queryEditor.transformToLowercase')).toContain('convert to lowercase');
expect(findEditorActionLabels('gonavi.selectCurrentStatement')).toContain('GoNavi: Select Current Line and Copy');
expect(findEditorActionLabels('gonavi.duplicateCurrentLine')).toContain('GoNavi: Duplicate Current Line Below');
expect(findEditorActionLabels('gonavi.saveQuery')).toContain('GoNavi: Save Query');
@@ -4909,6 +4967,12 @@ describe('QueryEditor external SQL save', () => {
expect(findEditorAction('gonavi.insertSqlSnippet')).toMatchObject({
label: 'Insert SQL Snippet',
});
expect(findEditorAction('gonavi.queryEditor.transformToUppercase')).toMatchObject({
label: 'convert to uppercase',
});
expect(findEditorAction('gonavi.queryEditor.transformToLowercase')).toMatchObject({
label: 'convert to lowercase',
});
expect(findEditorAction('gonavi.selectCurrentStatement')).toMatchObject({
label: 'GoNavi: Select Current Line and Copy',
});

View File

@@ -1365,6 +1365,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const triggerSqlAiCompletionActionRef = useRef<any>(null);
const triggerSqlAiCompletionKeydownDisposableRef = useRef<any>(null);
const insertSqlSnippetActionRef = useRef<any>(null);
const transformCaseActionDisposablesRef = useRef<any[]>([]);
const aiContextMenuActionDisposablesRef = useRef<any[]>([]);
const toggleQueryResultsPanelActionRef = useRef<any>(null);
const lastExternalQueryRef = useRef<string>(getTabQueryValue(tab));
@@ -1629,6 +1630,36 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
});
}, [handleOpenSqlSnippetPicker]);
const disposeTransformCaseContextMenuActions = useCallback(() => {
transformCaseActionDisposablesRef.current.forEach((disposable) => disposable?.dispose?.());
transformCaseActionDisposablesRef.current = [];
}, []);
const registerTransformCaseContextMenuActions = useCallback((editor: any) => {
disposeTransformCaseContextMenuActions();
transformCaseActionDisposablesRef.current = [
{
id: 'gonavi.queryEditor.transformToUppercase',
label: translate('query_editor.completion.action.uppercase'),
actionId: 'editor.action.transformToUppercase',
contextMenuOrder: 1,
},
{
id: 'gonavi.queryEditor.transformToLowercase',
label: translate('query_editor.completion.action.lowercase'),
actionId: 'editor.action.transformToLowercase',
contextMenuOrder: 2,
},
].map((action) => editor.addAction({
id: action.id,
label: action.label,
precondition: '!editorReadonly',
contextMenuGroupId: '1_modification',
contextMenuOrder: action.contextMenuOrder,
run: (ed: any) => ed.getAction?.(action.actionId)?.run?.(),
}));
}, [disposeTransformCaseContextMenuActions]);
// SQL 诊断 / 慢 SQL 历史的快捷键监听(必须在 binding 声明之后)
useEffect(() => {
if (!isActive) return;
@@ -4795,6 +4826,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
sharedActiveEditorModelUri = '';
}
disposeQueryEditorAiContextMenuActions();
disposeTransformCaseContextMenuActions();
window.removeEventListener('keydown', syncModifierState);
window.removeEventListener('keyup', syncModifierState);
window.removeEventListener('blur', handleWindowBlur);
@@ -4811,6 +4843,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
// 注册 AI 右键菜单操作
registerQueryEditorAiContextMenuActions(editor);
registerInsertSqlSnippetContextMenuAction(editor);
registerTransformCaseContextMenuActions(editor);
registerTriggerSqlAiCompletionAction(editor, monaco);
// Register runQuery shortcut inside Monaco so it overrides Monaco's default keybinding
@@ -7739,6 +7772,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
}, [languagePreference, registerInsertSqlSnippetContextMenuAction]);
useEffect(() => {
const editor = editorRef.current;
if (!editor) return;
registerTransformCaseContextMenuActions(editor);
return () => {
disposeTransformCaseContextMenuActions();
};
}, [languagePreference, disposeTransformCaseContextMenuActions, registerTransformCaseContextMenuActions]);
useEffect(() => {
const editor = editorRef.current;
const monaco = monacoRef.current;