mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
🐛 fix(query-editor): 修复 MySQL 关键字需美化后才着色
- 根据当前连接方言为 MySQL 系查询启用 Monaco MySQL 语法 - 将补全、悬浮、斜杠命令和代码片段同步注册到 MySQL 语言 - 增加方言映射和首屏着色回归测试 Fixes #612
This commit is contained in:
@@ -170,6 +170,7 @@ const autoFetchState = vi.hoisted(() => ({
|
||||
|
||||
const monacoEditorMockState = vi.hoisted(() => ({
|
||||
deferOnMount: false,
|
||||
latestProps: null as any,
|
||||
}));
|
||||
|
||||
const defaultEditorContributionResolver = (state: {
|
||||
@@ -199,7 +200,9 @@ const editorState = vi.hoisted(() => {
|
||||
position: { lineNumber: 1, column: 1 },
|
||||
selection: null as any,
|
||||
providers: [] as any[],
|
||||
providerLanguages: [] as string[],
|
||||
hoverProviders: [] as any[],
|
||||
hoverProviderLanguages: [] as string[],
|
||||
contentChangeListeners: [] as Array<() => void>,
|
||||
cursorPositionListeners: [] as Array<(event: any) => void>,
|
||||
modelContentListeners: [] as Array<(event: any) => void>,
|
||||
@@ -365,7 +368,9 @@ vi.mock('../utils/autoFetchVisibility', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@monaco-editor/react', () => ({
|
||||
default: ({ defaultValue, onChange, onMount }: any) => {
|
||||
default: (props: any) => {
|
||||
const { defaultValue, onChange, onMount } = props;
|
||||
monacoEditorMockState.latestProps = props;
|
||||
React.useEffect(() => {
|
||||
editorState.value = String(defaultValue || '');
|
||||
editorState.latestOnChange = onChange;
|
||||
@@ -376,11 +381,13 @@ vi.mock('@monaco-editor/react', () => ({
|
||||
languages: {
|
||||
CompletionItemKind: { Keyword: 1, Function: 2, Field: 3 },
|
||||
CompletionItemInsertTextRule: { InsertAsSnippet: 1 },
|
||||
registerCompletionItemProvider: vi.fn((_language: string, provider: any) => {
|
||||
registerCompletionItemProvider: vi.fn((language: string, provider: any) => {
|
||||
editorState.providerLanguages.push(language);
|
||||
editorState.providers.push(provider);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
registerHoverProvider: vi.fn((_language: string, provider: any) => {
|
||||
registerHoverProvider: vi.fn((language: string, provider: any) => {
|
||||
editorState.hoverProviderLanguages.push(language);
|
||||
editorState.hoverProviders.push(provider);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
@@ -865,9 +872,12 @@ describe('QueryEditor external SQL save', () => {
|
||||
editorState.value = '';
|
||||
editorState.position = { lineNumber: 1, column: 1 };
|
||||
editorState.selection = null;
|
||||
monacoEditorMockState.latestProps = null;
|
||||
editorState.domNode.style.cursor = '';
|
||||
editorState.providers = [];
|
||||
editorState.providerLanguages = [];
|
||||
editorState.hoverProviders = [];
|
||||
editorState.hoverProviderLanguages = [];
|
||||
editorState.contentChangeListeners = [];
|
||||
editorState.cursorPositionListeners = [];
|
||||
editorState.modelContentListeners = [];
|
||||
@@ -2369,9 +2379,34 @@ describe('QueryEditor external SQL save', () => {
|
||||
|
||||
const completionState = (globalThis as any).__gonaviSqlCompletionState;
|
||||
|
||||
expect(editorState.hoverProviders).toHaveLength(1);
|
||||
expect(editorState.providers).toHaveLength(3);
|
||||
expect(completionState.disposables).toHaveLength(4);
|
||||
expect(editorState.hoverProviderLanguages).toEqual(['sql', 'mysql']);
|
||||
expect(editorState.providerLanguages).toEqual(['sql', 'mysql', 'sql', 'mysql', 'sql', 'mysql']);
|
||||
expect(editorState.hoverProviders).toHaveLength(2);
|
||||
expect(editorState.providers).toHaveLength(6);
|
||||
expect(completionState.disposables).toHaveLength(8);
|
||||
|
||||
await act(async () => {
|
||||
renderer.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['mysql', 'mysql'],
|
||||
['mariadb', 'mysql'],
|
||||
['postgres', 'sql'],
|
||||
])('uses the %s connection grammar before formatting SQL', async (dbType, expectedLanguage) => {
|
||||
storeState.connections[0].config.type = dbType;
|
||||
const initialSql = "update finan_ set openid = 'ol_' where pay_status = '0' limit 50";
|
||||
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ query: initialSql })} />);
|
||||
});
|
||||
|
||||
expect(monacoEditorMockState.latestProps).toMatchObject({
|
||||
defaultValue: initialSql,
|
||||
language: expectedLanguage,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderer.unmount();
|
||||
@@ -6122,7 +6157,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(editorState.hoverProviders).toHaveLength(1);
|
||||
expect(editorState.hoverProviders).toHaveLength(2);
|
||||
const hover = editorState.hoverProviders[0].provideHover(
|
||||
editorState.editor.getModel(),
|
||||
{ lineNumber: 1, column: 18 },
|
||||
|
||||
@@ -160,6 +160,7 @@ import {
|
||||
resolveOracleLikeExecutionSchemaName,
|
||||
resolveOracleLikeLookupSchemaCandidates,
|
||||
resolveQueryEditorFormatterLanguage,
|
||||
resolveQueryEditorMonacoLanguage,
|
||||
resolveQueryEditorHoverTarget,
|
||||
resolveQueryEditorNavigationDecorations,
|
||||
resolveQueryEditorNavigationTarget,
|
||||
@@ -886,7 +887,8 @@ const resolveQueryEditorAiConnectionHost = (connection: any): string => {
|
||||
|
||||
// HMR 重载时释放旧注册避免补全和 hover 内容重复
|
||||
const _g = globalThis as any;
|
||||
const SQL_COMPLETION_PROVIDER_VERSION = '20260715-oracle-view-synonym-v1';
|
||||
const SQL_COMPLETION_PROVIDER_VERSION = '20260718-mysql-language-v1';
|
||||
const QUERY_EDITOR_MONACO_LANGUAGE_IDS = ['sql', 'mysql'] as const;
|
||||
if (!_g.__gonaviSqlCompletionState) {
|
||||
_g.__gonaviSqlCompletionState = { registered: false, version: '', disposables: [] as any[] };
|
||||
}
|
||||
@@ -1379,6 +1381,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
),
|
||||
[connections, currentConnectionId],
|
||||
);
|
||||
const queryEditorMonacoLanguage = useMemo(
|
||||
() => resolveQueryEditorMonacoLanguage(
|
||||
connections.find(connection => connection.id === currentConnectionId),
|
||||
),
|
||||
[connections, currentConnectionId],
|
||||
);
|
||||
|
||||
const addSqlLog = useStore(state => state.addSqlLog);
|
||||
const sqlLogCount = useStore(state => state.sqlLogs.length);
|
||||
@@ -4860,7 +4868,17 @@ 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', {
|
||||
const registerQueryEditorHoverProvider = (provider: any) => {
|
||||
QUERY_EDITOR_MONACO_LANGUAGE_IDS.forEach((languageId) => {
|
||||
sqlCompletionDisposables.push(monaco.languages.registerHoverProvider(languageId, provider));
|
||||
});
|
||||
};
|
||||
const registerQueryEditorCompletionProvider = (provider: any) => {
|
||||
QUERY_EDITOR_MONACO_LANGUAGE_IDS.forEach((languageId) => {
|
||||
sqlCompletionDisposables.push(monaco.languages.registerCompletionItemProvider(languageId, provider));
|
||||
});
|
||||
};
|
||||
registerQueryEditorHoverProvider({
|
||||
provideHover: (model: any, position: any) => {
|
||||
const normalizedPosition = normalizeEditorPosition(position);
|
||||
if (!normalizedPosition) {
|
||||
@@ -4896,8 +4914,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
contents: [{ value: buildQueryEditorHoverMarkdown(hoverTarget) }],
|
||||
};
|
||||
},
|
||||
}));
|
||||
sqlCompletionDisposables.push(monaco.languages.registerCompletionItemProvider('sql', {
|
||||
});
|
||||
registerQueryEditorCompletionProvider({
|
||||
triggerCharacters: ['.'],
|
||||
provideCompletionItems: async (model: any, position: any, _context?: any, token?: { isCancellationRequested?: boolean }) => {
|
||||
if (isSqlCompletionRequestCancelled(token)) {
|
||||
@@ -5792,8 +5810,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
];
|
||||
return { suggestions };
|
||||
}
|
||||
}));
|
||||
sqlCompletionDisposables.push(monaco.languages.registerCompletionItemProvider('sql', {
|
||||
});
|
||||
registerQueryEditorCompletionProvider({
|
||||
triggerCharacters: ['/'],
|
||||
provideCompletionItems: (model: any, position: any) => {
|
||||
const lineContent = model.getLineContent(position.lineNumber);
|
||||
@@ -5820,11 +5838,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
})),
|
||||
};
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
// SQL snippet completion provider
|
||||
sqlCompletionDisposables.push(monaco.languages.registerCompletionItemProvider('sql', {
|
||||
registerQueryEditorCompletionProvider({
|
||||
provideCompletionItems: (model: any, position: any) => {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const prefix = word.word.toLowerCase();
|
||||
@@ -5856,7 +5874,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
})),
|
||||
};
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
} // end sqlCompletionRegistered guard
|
||||
|
||||
@@ -8710,7 +8728,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
<Editor
|
||||
height="100%"
|
||||
gonaviTypography="code"
|
||||
defaultLanguage="sql"
|
||||
language={queryEditorMonacoLanguage}
|
||||
theme={darkMode ? "transparent-dark" : "transparent-light"}
|
||||
defaultValue={query}
|
||||
onChange={(val) => {
|
||||
|
||||
@@ -7,11 +7,25 @@ import {
|
||||
resolveOracleLikeDefaultSchemaName,
|
||||
resolveOracleLikeExecutionSchemaName,
|
||||
resolveOracleLikeLookupSchemaCandidates,
|
||||
resolveQueryEditorMonacoLanguage,
|
||||
resolveQueryEditorNavigationTarget,
|
||||
selectUnqualifiedCompletionSynonyms,
|
||||
shouldHandleQueryEditorRunShortcutFallback,
|
||||
} from './QueryEditorHelpers';
|
||||
|
||||
describe('QueryEditor Monaco SQL grammar', () => {
|
||||
it.each([
|
||||
[{ config: { type: 'mysql' } }, 'mysql'],
|
||||
[{ config: { type: 'mariadb' } }, 'mysql'],
|
||||
[{ config: { type: 'custom', driver: 'greatdb' } }, 'mysql'],
|
||||
[{ config: { type: 'oceanbase', oceanBaseProtocol: 'mysql' } }, 'mysql'],
|
||||
[{ config: { type: 'oceanbase', oceanBaseProtocol: 'oracle' } }, 'sql'],
|
||||
[{ config: { type: 'postgres' } }, 'sql'],
|
||||
])('maps connection row %# to the expected Monaco grammar', (connection, expectedLanguage) => {
|
||||
expect(resolveQueryEditorMonacoLanguage(connection)).toBe(expectedLanguage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('QueryEditor run shortcut routing', () => {
|
||||
it('reserves editor-originated shortcuts for Monaco and keeps document targets as a fallback', () => {
|
||||
const editorTarget = {} as Node;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SqlLanguage } from 'sql-formatter';
|
||||
import type { TabData, ColumnDefinition, IndexDefinition } from '../../types';
|
||||
import { DBGetColumns, DBGetIndexes, DBQuery } from '../../../wailsjs/go/app/App';
|
||||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
|
||||
import { isOracleLikeDialect, resolveSqlDialect } from '../../utils/sqlDialect';
|
||||
import { isMysqlFamilyDialect, isOracleLikeDialect, resolveSqlDialect } from '../../utils/sqlDialect';
|
||||
import { extractQueryResultTableRef, type QueryResultTableRef } from '../../utils/queryResultTable';
|
||||
import { quoteIdentPart } from '../../utils/sql';
|
||||
import { splitSidebarQualifiedName } from '../../utils/sidebarLocate';
|
||||
@@ -572,6 +572,17 @@ export const normalizeMetadataDialect = (conn: any): string => {
|
||||
return String(dialect || '').toLowerCase();
|
||||
};
|
||||
|
||||
export type QueryEditorMonacoLanguage = 'sql' | 'mysql';
|
||||
|
||||
export const resolveQueryEditorMonacoLanguage = (conn: any): QueryEditorMonacoLanguage => {
|
||||
const dialect = resolveSqlDialect(
|
||||
String(conn?.config?.type || ''),
|
||||
String(conn?.config?.driver || ''),
|
||||
{ oceanBaseProtocol: conn?.config?.oceanBaseProtocol },
|
||||
);
|
||||
return isMysqlFamilyDialect(dialect) ? 'mysql' : 'sql';
|
||||
};
|
||||
|
||||
export const resolveQueryEditorFormatterLanguage = (conn: any): SqlLanguage => {
|
||||
const dialect = normalizeMetadataDialect(conn);
|
||||
switch (dialect) {
|
||||
|
||||
Reference in New Issue
Block a user