mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-19 22:15:12 +08:00
🐛 fix(query-editor): 修复快捷执行与 Windows 输入丢字
- 快捷执行仅由 Monaco 编辑器处理,保留 Shift+Home 反向选区和光标位置 - 缓冲并恢复 Windows 下丢失的可打印输入和延迟 IME 提交 - 降低 AI 行内补全预检与元数据刷新造成的编辑器输入抖动 - 补充快捷键、输入回退和 AI 补全回归测试
This commit is contained in:
@@ -497,6 +497,36 @@ describe('QueryEditorAiAssist', () => {
|
||||
expect(missingProvider.reason).toBe('provider_missing');
|
||||
});
|
||||
|
||||
it('caches unavailable inline AI readiness across adjacent automatic requests', async () => {
|
||||
const service: QueryEditorAiService = {
|
||||
AIChatSend: vi.fn(),
|
||||
AIGetProviders: vi.fn(async () => []),
|
||||
AIGetActiveProvider: vi.fn(async () => ''),
|
||||
};
|
||||
const request = {
|
||||
service,
|
||||
aiContext: {
|
||||
connectionName: 'Local MySQL',
|
||||
sourceType: 'mysql',
|
||||
currentDb: 'shop',
|
||||
tables: [],
|
||||
columns: [],
|
||||
},
|
||||
editorSnapshot: {
|
||||
prefix: 'select * from users ',
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: 'select * from users ',
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
};
|
||||
|
||||
await requestQueryEditorInlineCompletion(request);
|
||||
await requestQueryEditorInlineCompletion(request);
|
||||
|
||||
expect(service.AIGetProviders).toHaveBeenCalledTimes(1);
|
||||
expect(service.AIGetActiveProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('uses deterministic schema metadata for table-name inline completion and skips AI', async () => {
|
||||
const service = readyService('SELECT * FROM orders;');
|
||||
|
||||
@@ -522,6 +552,8 @@ describe('QueryEditorAiAssist', () => {
|
||||
|
||||
expect(insertText).toBe('eos');
|
||||
expect(service.AIChatSend).not.toHaveBeenCalled();
|
||||
expect(service.AIGetProviders).not.toHaveBeenCalled();
|
||||
expect(service.AIGetActiveProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses deterministic schema metadata for alter-table inline completion and skips AI', async () => {
|
||||
|
||||
@@ -107,6 +107,7 @@ const MAX_INLINE_INSERT_CHARS = 1800;
|
||||
const MAX_INLINE_GHOST_PREVIEW_CHARS = 220;
|
||||
const INLINE_COMPLETION_MAX_TOKENS = 192;
|
||||
const INLINE_COMPLETION_TEMPERATURE = 0.1;
|
||||
const INLINE_RUNTIME_READINESS_CACHE_TTL_MS = 5000;
|
||||
|
||||
const SQL_CODE_FENCE_RE = /```(?:sql|mysql|postgresql|postgres|oracle|plsql|sqlite|sqlserver|mssql|tsql|clickhouse|duckdb|starrocks|tdengine)?\s*([\s\S]*?)```/i;
|
||||
const INLINE_TABLE_COMPLETION_RE = /\b(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM|ALTER\s+TABLE|DROP\s+TABLE|TRUNCATE\s+TABLE)\s*([^\s,()]*)$/i;
|
||||
@@ -174,6 +175,49 @@ export const resolveQueryEditorAiRuntimeReadiness = async (
|
||||
};
|
||||
};
|
||||
|
||||
type InlineRuntimeReadinessCacheEntry = {
|
||||
expiresAt: number;
|
||||
promise: Promise<QueryEditorAiRuntimeReadiness>;
|
||||
};
|
||||
|
||||
let inlineRuntimeReadinessCache = new WeakMap<object, InlineRuntimeReadinessCacheEntry>();
|
||||
|
||||
export const clearQueryEditorInlineRuntimeReadinessCache = (): void => {
|
||||
inlineRuntimeReadinessCache = new WeakMap<object, InlineRuntimeReadinessCacheEntry>();
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('gonavi:ai:provider-changed', clearQueryEditorInlineRuntimeReadinessCache);
|
||||
window.addEventListener('gonavi:ai:config-changed', clearQueryEditorInlineRuntimeReadinessCache);
|
||||
}
|
||||
|
||||
export const resolveQueryEditorInlineRuntimeReadiness = (
|
||||
service: QueryEditorAiService | undefined,
|
||||
): Promise<QueryEditorAiRuntimeReadiness> => {
|
||||
if (!service || typeof service !== 'object') {
|
||||
return resolveQueryEditorAiRuntimeReadiness(service, { requireInlineCompletionModel: true });
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const cached = inlineRuntimeReadinessCache.get(service);
|
||||
if (cached && cached.expiresAt > now) {
|
||||
return cached.promise;
|
||||
}
|
||||
|
||||
const promise = resolveQueryEditorAiRuntimeReadiness(service, { requireInlineCompletionModel: true });
|
||||
const entry = {
|
||||
expiresAt: now + INLINE_RUNTIME_READINESS_CACHE_TTL_MS,
|
||||
promise,
|
||||
};
|
||||
inlineRuntimeReadinessCache.set(service, entry);
|
||||
void promise.catch(() => {
|
||||
if (inlineRuntimeReadinessCache.get(service) === entry) {
|
||||
inlineRuntimeReadinessCache.delete(service);
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
export const shouldRequestQueryEditorInlineCompletion = (
|
||||
snapshot: QueryEditorAiEditorSnapshot,
|
||||
): boolean => {
|
||||
@@ -259,6 +303,56 @@ export const resolveQueryEditorInlineMemoryInsertText = ({
|
||||
return '';
|
||||
};
|
||||
|
||||
export const resolveQueryEditorInlineLocalCompletion = ({
|
||||
aiContext,
|
||||
editorSnapshot,
|
||||
deferEmptySchemaCompletion = false,
|
||||
}: {
|
||||
aiContext: QueryEditorAiContext;
|
||||
editorSnapshot: QueryEditorAiEditorSnapshot;
|
||||
deferEmptySchemaCompletion?: boolean;
|
||||
}): { handled: boolean; insertText: string } => {
|
||||
if (!shouldRequestQueryEditorInlineCompletion(editorSnapshot)) {
|
||||
return {
|
||||
handled: true,
|
||||
insertText: '',
|
||||
};
|
||||
}
|
||||
|
||||
const inlineIntent = resolveQueryEditorInlineCompletionIntentDetails(editorSnapshot);
|
||||
const deterministicCompletion = resolveDeterministicInlineSchemaCompletion(
|
||||
aiContext,
|
||||
editorSnapshot,
|
||||
inlineIntent,
|
||||
);
|
||||
if (deterministicCompletion.handled && deterministicCompletion.insertText) {
|
||||
return deterministicCompletion;
|
||||
}
|
||||
if (deterministicCompletion.handled) {
|
||||
if (deferEmptySchemaCompletion) {
|
||||
return {
|
||||
handled: false,
|
||||
insertText: '',
|
||||
};
|
||||
}
|
||||
if (
|
||||
(inlineIntent.intent === 'table_name'
|
||||
&& !shouldAllowInlineTableAiFallback(aiContext, inlineIntent.fragment))
|
||||
|| (inlineIntent.intent === 'column_name'
|
||||
&& !shouldAllowInlineColumnAiFallback(
|
||||
aiContext,
|
||||
editorSnapshot,
|
||||
inlineIntent.qualifier,
|
||||
inlineIntent.fragment,
|
||||
))
|
||||
) {
|
||||
return deterministicCompletion;
|
||||
}
|
||||
}
|
||||
|
||||
return resolveDeterministicInlineSyntaxCompletion(editorSnapshot);
|
||||
};
|
||||
|
||||
export const requestQueryEditorInlineCompletion = async ({
|
||||
service,
|
||||
aiContext,
|
||||
@@ -268,35 +362,12 @@ export const requestQueryEditorInlineCompletion = async ({
|
||||
aiContext: QueryEditorAiContext;
|
||||
editorSnapshot: QueryEditorAiEditorSnapshot;
|
||||
}): Promise<string> => {
|
||||
if (!shouldRequestQueryEditorInlineCompletion(editorSnapshot)) {
|
||||
return '';
|
||||
const localCompletion = resolveQueryEditorInlineLocalCompletion({ aiContext, editorSnapshot });
|
||||
if (localCompletion.handled) {
|
||||
return localCompletion.insertText;
|
||||
}
|
||||
|
||||
const inlineIntent = resolveQueryEditorInlineCompletionIntentDetails(editorSnapshot);
|
||||
const deterministicCompletion = resolveDeterministicInlineSchemaCompletion(aiContext, editorSnapshot, inlineIntent);
|
||||
if (deterministicCompletion.handled && deterministicCompletion.insertText) {
|
||||
return deterministicCompletion.insertText;
|
||||
}
|
||||
if (deterministicCompletion.handled) {
|
||||
if (inlineIntent.intent === 'table_name') {
|
||||
if (!shouldAllowInlineTableAiFallback(aiContext, inlineIntent.fragment)) {
|
||||
return '';
|
||||
}
|
||||
} else if (inlineIntent.intent === 'column_name') {
|
||||
if (!shouldAllowInlineColumnAiFallback(aiContext, editorSnapshot, inlineIntent.qualifier, inlineIntent.fragment)) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deterministicSyntaxCompletion = resolveDeterministicInlineSyntaxCompletion(editorSnapshot);
|
||||
if (deterministicSyntaxCompletion.handled) {
|
||||
return deterministicSyntaxCompletion.insertText;
|
||||
}
|
||||
|
||||
const readiness = await resolveQueryEditorAiRuntimeReadiness(service, {
|
||||
requireInlineCompletionModel: true,
|
||||
});
|
||||
const readiness = await resolveQueryEditorInlineRuntimeReadiness(service);
|
||||
if (!readiness.ready || !readiness.provider) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -6,8 +6,34 @@ import {
|
||||
resolveOracleLikeExecutionSchemaName,
|
||||
resolveOracleLikeLookupSchemaCandidates,
|
||||
resolveQueryEditorNavigationTarget,
|
||||
shouldHandleQueryEditorRunShortcutFallback,
|
||||
} from './QueryEditorHelpers';
|
||||
|
||||
describe('QueryEditor run shortcut routing', () => {
|
||||
it('reserves editor-originated shortcuts for Monaco and keeps document targets as a fallback', () => {
|
||||
const editorTarget = {} as Node;
|
||||
const editorPane = {
|
||||
contains: (node: Node) => node === editorTarget,
|
||||
} as Pick<Node, 'contains'>;
|
||||
|
||||
expect(shouldHandleQueryEditorRunShortcutFallback({
|
||||
editorHasFocus: true,
|
||||
targetNode: editorTarget,
|
||||
editorPane,
|
||||
})).toBe(false);
|
||||
expect(shouldHandleQueryEditorRunShortcutFallback({
|
||||
editorHasFocus: true,
|
||||
targetNode: null,
|
||||
editorPane,
|
||||
})).toBe(true);
|
||||
expect(shouldHandleQueryEditorRunShortcutFallback({
|
||||
editorHasFocus: false,
|
||||
targetNode: null,
|
||||
editorPane,
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('QueryEditorHelpers Oracle-like execution schema', () => {
|
||||
it('uses the selected schema when it differs from the login user', () => {
|
||||
const config = {
|
||||
|
||||
@@ -2283,6 +2283,24 @@ export const isDocumentLevelShortcutTarget = (targetNode: Node | null): boolean
|
||||
return targetNode === document.body || targetNode === document.documentElement;
|
||||
};
|
||||
|
||||
export const shouldHandleQueryEditorRunShortcutFallback = ({
|
||||
editorHasFocus,
|
||||
targetNode,
|
||||
editorPane,
|
||||
}: {
|
||||
editorHasFocus: boolean;
|
||||
targetNode: Node | null;
|
||||
editorPane?: Pick<Node, 'contains'> | null;
|
||||
}): boolean => {
|
||||
if (!editorHasFocus) {
|
||||
return false;
|
||||
}
|
||||
if (targetNode && editorPane?.contains(targetNode)) {
|
||||
return false;
|
||||
}
|
||||
return isDocumentLevelShortcutTarget(targetNode);
|
||||
};
|
||||
|
||||
export const clearQueryEditorLinkDecorations = (
|
||||
editor: any,
|
||||
decorationIdsRef: React.MutableRefObject<string[]>,
|
||||
|
||||
Reference in New Issue
Block a user