mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 16:53:35 +08:00
🐛 fix(query-editor): 修复补全标识符大小写
This commit is contained in:
@@ -1964,9 +1964,18 @@ describe('QueryEditor external SQL save', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts the AI inline ghost with the default Tab shortcut and consumes the keydown', async () => {
|
||||
it('accepts a metadata-normalized inline ghost with the default Tab shortcut and preserves trailing SQL', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
storeState.sqlLogs = [{
|
||||
id: 'sql-log-inline-case',
|
||||
timestamp: Date.now(),
|
||||
sql: 'SELECT * FROM a_cninfo_announcement WHERE id = 1;',
|
||||
status: 'success',
|
||||
duration: 12,
|
||||
dbName: 'main',
|
||||
} as any];
|
||||
|
||||
const inlineAiService = {
|
||||
AIGetProviders: vi.fn(async () => [{
|
||||
id: 'openai-main',
|
||||
@@ -1991,8 +2000,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
backendApp.DBGetTables.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [
|
||||
{ TABLE_NAME: 'videos' },
|
||||
{ TABLE_NAME: 'visits' },
|
||||
{ TABLE_NAME: 'a_cninfo_announcement' },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -2020,19 +2028,19 @@ describe('QueryEditor external SQL save', () => {
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: 'SELECT', dbName: 'main' })} />);
|
||||
create(<QueryEditor tab={createTab({ query: 'SELECT * FROM A_C', dbName: 'main' })} />);
|
||||
});
|
||||
|
||||
editorState.value = 'SELECT';
|
||||
editorState.position = { lineNumber: 1, column: 'SELECT'.length + 1 };
|
||||
editorState.value = 'SELECT * FROM A_C';
|
||||
editorState.position = { lineNumber: 1, column: 'SELECT * FROM A_C'.length + 1 };
|
||||
editorState.editor.executeEdits.mockClear();
|
||||
editorState.editor.trigger.mockClear();
|
||||
editorState.domNode.appendChild.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
editorState.latestOnChange?.('SELECT');
|
||||
editorState.latestOnChange?.('SELECT * FROM A_C');
|
||||
editorState.modelContentListeners.forEach((listener) => listener({
|
||||
changes: [{ text: 'T' }],
|
||||
changes: [{ text: 'C' }],
|
||||
}));
|
||||
vi.advanceTimersByTime(220);
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
@@ -2040,6 +2048,11 @@ describe('QueryEditor external SQL save', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const ghostOverlay = editorState.domNode.appendChild.mock.calls[
|
||||
editorState.domNode.appendChild.mock.calls.length - 1
|
||||
]?.[0];
|
||||
expect(ghostOverlay?.textContent).toBe('ninfo_announcement WHERE id = 1;');
|
||||
|
||||
const shortcutEvent = {
|
||||
type: 'keydown',
|
||||
key: 'Tab',
|
||||
@@ -2069,8 +2082,16 @@ describe('QueryEditor external SQL save', () => {
|
||||
|
||||
expect(editorState.editor.executeEdits).toHaveBeenCalledWith(
|
||||
'gonavi-ai-inline-sql-completion',
|
||||
[expect.objectContaining({ text: expect.any(String) })],
|
||||
[expect.objectContaining({
|
||||
text: 'a_cninfo_announcement WHERE id = 1;',
|
||||
range: expect.objectContaining({
|
||||
startColumn: 15,
|
||||
endColumn: 18,
|
||||
}),
|
||||
})],
|
||||
);
|
||||
expect(editorState.value).toBe('SELECT * FROM a_cninfo_announcement WHERE id = 1;');
|
||||
expect(inlineAiService.AIChatSend).not.toHaveBeenCalled();
|
||||
expect(monacoShortcutEvent.preventDefault).toHaveBeenCalled();
|
||||
expect(monacoShortcutEvent.stopPropagation).toHaveBeenCalled();
|
||||
expect(shortcutEvent.preventDefault).toHaveBeenCalled();
|
||||
@@ -3447,6 +3468,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
success: true,
|
||||
data: [
|
||||
{ Tables_in_main: 'users' },
|
||||
{ Tables_in_main: 'a_cninfo_announcement' },
|
||||
{ Tables_in_main: 'hrmresource' },
|
||||
{ Tables_in_main: 'hrm_resource_export_template' },
|
||||
{ Tables_in_main: 'archive_hrmresource' },
|
||||
@@ -3457,6 +3479,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
data: [
|
||||
{ tableName: 'hrmresource', name: 'hrmresult', type: 'varchar(32)' },
|
||||
{ tableName: 'users', name: 'hrmresult_from_users', type: 'varchar(32)' },
|
||||
{ tableName: 'users', name: 'SHORT_TITLE', type: 'varchar(255)' },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3494,6 +3517,24 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(commaLabels).not.toContain('hrmresult_from_users');
|
||||
expect(backendApp.DBGetColumns.mock.calls.map((call: any[]) => call[2])).not.toContain('hrmres');
|
||||
|
||||
editorState.value = 'SELECT * FROM A_C';
|
||||
editorState.latestOnChange?.(editorState.value);
|
||||
const uppercaseTableResult = await sqlProvider.provideCompletionItems(
|
||||
editorState.editor.getModel(),
|
||||
{ lineNumber: 1, column: editorState.value.length + 1 },
|
||||
);
|
||||
const uppercaseTable = uppercaseTableResult.suggestions.find((item: any) => item.label === 'a_cninfo_announcement');
|
||||
expect(uppercaseTable?.insertText).toBe('a_cninfo_announcement');
|
||||
|
||||
editorState.value = 'SELECT * FROM users WHERE sh';
|
||||
editorState.latestOnChange?.(editorState.value);
|
||||
const lowercaseColumnResult = await sqlProvider.provideCompletionItems(
|
||||
editorState.editor.getModel(),
|
||||
{ lineNumber: 1, column: editorState.value.length + 1 },
|
||||
);
|
||||
const lowercaseColumn = lowercaseColumnResult.suggestions.find((item: any) => item.label === 'SHORT_TITLE');
|
||||
expect(lowercaseColumn?.insertText).toBe('short_title');
|
||||
|
||||
await act(async () => {
|
||||
renderer.unmount();
|
||||
});
|
||||
|
||||
@@ -213,6 +213,7 @@ import {
|
||||
shouldHandleQueryEditorRunShortcutFallback,
|
||||
} from './queryEditor/QueryEditorHelpers';
|
||||
import {
|
||||
applyQueryEditorCompletionFragmentCase,
|
||||
buildQueryEditorAiInlineSuggestOptions,
|
||||
getQueryEditorAiService,
|
||||
requestQueryEditorInlineCompletion,
|
||||
@@ -221,6 +222,7 @@ import {
|
||||
resolveInlineSqlGhostPreviewText,
|
||||
resolveQueryEditorInlineMemoryInsertText,
|
||||
resolveQueryEditorInlineCompletionIntentDetails,
|
||||
resolveQueryEditorInlineCompletionEdit,
|
||||
resolveQueryEditorInlineLocalCompletion,
|
||||
resolveQueryEditorInlineRuntimeReadiness,
|
||||
shouldTriggerQueryEditorInlineObjectSuggestFallback,
|
||||
@@ -228,6 +230,7 @@ import {
|
||||
type QueryEditorAiApplyMode,
|
||||
type QueryEditorAiContext,
|
||||
type QueryEditorAiEditorSnapshot,
|
||||
type QueryEditorInlineCompletionEdit,
|
||||
} from './queryEditor/QueryEditorAiAssist';
|
||||
export {
|
||||
collectQueryEditorObjectDecorationCandidates,
|
||||
@@ -1559,6 +1562,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const triggerSqlAiCompletionFallbackApplyingRef = useRef(false);
|
||||
const aiInlineGhostRef = useRef<{
|
||||
insertText: string;
|
||||
editText: string;
|
||||
replacePrefixLength: number;
|
||||
modelUri: string;
|
||||
position: { lineNumber: number; column: number };
|
||||
snapshot: QueryEditorAiEditorSnapshot;
|
||||
@@ -4418,7 +4423,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return false;
|
||||
}
|
||||
const changes = Array.isArray(event?.changes) ? event.changes : [];
|
||||
return changes.some((change: any) => String(change?.text ?? '') === ghost.insertText);
|
||||
return changes.some((change: any) => {
|
||||
const changedText = String(change?.text ?? '');
|
||||
return changedText === ghost.insertText || changedText === ghost.editText;
|
||||
});
|
||||
};
|
||||
|
||||
const buildInlineGhostEditorSnapshot = (model: any, position: { lineNumber: number; column: number }): QueryEditorAiEditorSnapshot => {
|
||||
@@ -4527,8 +4535,14 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
position: { lineNumber: number; column: number },
|
||||
insertText: string,
|
||||
snapshot: QueryEditorAiEditorSnapshot,
|
||||
edit?: QueryEditorInlineCompletionEdit,
|
||||
) => {
|
||||
const previewText = resolveInlineSqlGhostPreviewText(insertText);
|
||||
const resolvedEdit = edit || {
|
||||
previewText: insertText,
|
||||
editText: insertText,
|
||||
replacePrefixLength: 0,
|
||||
};
|
||||
const previewText = resolveInlineSqlGhostPreviewText(resolvedEdit.previewText);
|
||||
if (!previewText) {
|
||||
clearAiInlineGhost(false);
|
||||
return;
|
||||
@@ -4536,7 +4550,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
|
||||
const modelUri = String(model?.uri?.toString?.() || '');
|
||||
aiInlineGhostRef.current = {
|
||||
insertText,
|
||||
insertText: resolvedEdit.previewText,
|
||||
editText: resolvedEdit.editText,
|
||||
replacePrefixLength: resolvedEdit.replacePrefixLength,
|
||||
modelUri,
|
||||
position,
|
||||
snapshot,
|
||||
@@ -4592,23 +4608,31 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
aiInlineGhostAcceptingRef.current = true;
|
||||
try {
|
||||
editor.pushUndoStop?.();
|
||||
const replacePrefixLength = Math.max(
|
||||
0,
|
||||
Math.min(ghost.replacePrefixLength, Math.max(0, position.column - 1)),
|
||||
);
|
||||
const editStartPosition = {
|
||||
lineNumber: position.lineNumber,
|
||||
column: position.column - replacePrefixLength,
|
||||
};
|
||||
const startOffset = typeof model.getOffsetAt === 'function'
|
||||
? Number(model.getOffsetAt(position))
|
||||
? Number(model.getOffsetAt(editStartPosition))
|
||||
: Number.NaN;
|
||||
editor.executeEdits?.('gonavi-ai-inline-sql-completion', [{
|
||||
range: new monaco.Range(
|
||||
position.lineNumber,
|
||||
position.column,
|
||||
editStartPosition.lineNumber,
|
||||
editStartPosition.column,
|
||||
position.lineNumber,
|
||||
position.column,
|
||||
),
|
||||
text: ghost.insertText,
|
||||
text: ghost.editText,
|
||||
forceMoveMarkers: true,
|
||||
}]);
|
||||
editor.pushUndoStop?.();
|
||||
syncQueryDraft(String(editor.getValue?.() ?? model.getValue?.() ?? ''));
|
||||
if (Number.isFinite(startOffset) && typeof model.getPositionAt === 'function') {
|
||||
const nextPosition = normalizeEditorPosition(model.getPositionAt(startOffset + ghost.insertText.length));
|
||||
const nextPosition = normalizeEditorPosition(model.getPositionAt(startOffset + ghost.editText.length));
|
||||
if (nextPosition) {
|
||||
editor.setPosition?.(nextPosition);
|
||||
}
|
||||
@@ -4652,20 +4676,26 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
}
|
||||
const intent = resolveQueryEditorInlineCompletionIntentDetails(editorSnapshot);
|
||||
const shouldUseInlineMemory = manualTrigger || intent.intent !== 'general_sql';
|
||||
let memoryInsertText = '';
|
||||
if (shouldUseInlineMemory) {
|
||||
const memoryInsertText = resolveQueryEditorInlineMemoryInsertText({
|
||||
const initialAiContext = buildQueryEditorAiContext();
|
||||
memoryInsertText = resolveQueryEditorInlineMemoryInsertText({
|
||||
editorSnapshot,
|
||||
memoryEntries: inlineSqlMemoryEntries,
|
||||
sourceType: initialAiContext.sourceType,
|
||||
});
|
||||
if (memoryInsertText.trim()) {
|
||||
renderAiInlineGhost(model, position, memoryInsertText, editorSnapshot);
|
||||
// Empty fragments do not need metadata-based case correction and retain
|
||||
// the previous immediate memory-completion behavior.
|
||||
if (memoryInsertText.trim() && !intent.fragment) {
|
||||
const memoryEdit = resolveQueryEditorInlineCompletionEdit({
|
||||
aiContext: initialAiContext,
|
||||
editorSnapshot,
|
||||
insertText: memoryInsertText,
|
||||
});
|
||||
renderAiInlineGhost(model, position, memoryEdit.previewText, editorSnapshot, memoryEdit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!shouldRequestQueryEditorInlineCompletion(editorSnapshot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = ++aiInlineGhostRequestSeqRef.current;
|
||||
const runRequest = () => {
|
||||
if (aiInlineGhostTimerRef.current !== null) {
|
||||
@@ -4679,6 +4709,41 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (shouldUseInlineMemory) {
|
||||
if (!memoryInsertText.trim()) {
|
||||
const initialAiContext = buildQueryEditorAiContext();
|
||||
memoryInsertText = resolveQueryEditorInlineMemoryInsertText({
|
||||
editorSnapshot,
|
||||
memoryEntries: inlineSqlMemoryEntries,
|
||||
sourceType: initialAiContext.sourceType,
|
||||
});
|
||||
}
|
||||
if (memoryInsertText.trim()) {
|
||||
if (
|
||||
(intent.intent === 'table_name' || intent.intent === 'column_name')
|
||||
&& intent.fragment
|
||||
) {
|
||||
await ensureQueryEditorAiContextMetadata(editorSnapshot);
|
||||
if (
|
||||
requestId !== aiInlineGhostRequestSeqRef.current
|
||||
|| editorRef.current !== editor
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const aiContext = buildQueryEditorAiContext();
|
||||
const memoryEdit = resolveQueryEditorInlineCompletionEdit({
|
||||
aiContext,
|
||||
editorSnapshot,
|
||||
insertText: memoryInsertText,
|
||||
});
|
||||
renderAiInlineGhost(model, position, memoryEdit.previewText, editorSnapshot, memoryEdit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!shouldRequestQueryEditorInlineCompletion(editorSnapshot)) {
|
||||
return;
|
||||
}
|
||||
const aiContext = buildQueryEditorAiContext();
|
||||
const localCompletion = resolveQueryEditorInlineLocalCompletion({
|
||||
aiContext,
|
||||
@@ -4687,7 +4752,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
});
|
||||
if (localCompletion.handled) {
|
||||
if (localCompletion.insertText.trim()) {
|
||||
renderAiInlineGhost(model, position, localCompletion.insertText, editorSnapshot);
|
||||
const localEdit = resolveQueryEditorInlineCompletionEdit({
|
||||
aiContext,
|
||||
editorSnapshot,
|
||||
insertText: localCompletion.insertText,
|
||||
});
|
||||
renderAiInlineGhost(model, position, localEdit.previewText, editorSnapshot, localEdit);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4735,7 +4805,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
}
|
||||
return;
|
||||
}
|
||||
renderAiInlineGhost(model, position, insertText, editorSnapshot);
|
||||
const inlineEdit = resolveQueryEditorInlineCompletionEdit({
|
||||
aiContext: buildQueryEditorAiContext(),
|
||||
editorSnapshot,
|
||||
insertText,
|
||||
});
|
||||
renderAiInlineGhost(model, position, inlineEdit.previewText, editorSnapshot, inlineEdit);
|
||||
} catch (error) {
|
||||
console.warn('GoNavi AI inline SQL ghost failed', error);
|
||||
}
|
||||
@@ -4803,7 +4878,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
clearAiInlineGhost();
|
||||
return;
|
||||
}
|
||||
renderAiInlineGhost(model, ghost.position, ghost.insertText, ghost.snapshot);
|
||||
renderAiInlineGhost(model, ghost.position, ghost.insertText, ghost.snapshot, {
|
||||
previewText: ghost.insertText,
|
||||
editText: ghost.editText,
|
||||
replacePrefixLength: ghost.replacePrefixLength,
|
||||
});
|
||||
};
|
||||
|
||||
const applyNavigationHoverStateAtPosition = (targetPosition: { lineNumber: number; column: number } | null) => {
|
||||
@@ -5710,6 +5789,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
if (!raw) return raw;
|
||||
return shouldQuoteCompletionIdentifiers ? quoteQualifiedIdent(activeDialect, raw) : raw;
|
||||
};
|
||||
const applyCompletionFragmentCase = (ident: string, fragment: string) => (
|
||||
shouldQuoteCompletionIdentifiers
|
||||
? ident
|
||||
: applyQueryEditorCompletionFragmentCase(ident, fragment)
|
||||
);
|
||||
const getActiveCompletionDbName = () => String(sharedCurrentDb || currentDbRef.current || currentDb || tab.dbName || '').trim();
|
||||
const dialectKeywords = resolveSqlKeywords(activeDialect);
|
||||
const dialectFunctions = resolveSqlFunctions(activeDialect);
|
||||
@@ -5725,14 +5809,16 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
&& !!parsed.table
|
||||
&& parsed.schema.toLowerCase() === rawDbName.toLowerCase();
|
||||
const displayName = schemaMatchesDb ? parsed.table : rawTableName;
|
||||
const insertName = schemaMatchesDb ? parsed.table : rawTableName;
|
||||
const insertText = schemaMatchesDb
|
||||
? quoteCompletionPart(parsed.table)
|
||||
: quoteCompletionPath(rawTableName);
|
||||
? quoteCompletionPart(insertName)
|
||||
: quoteCompletionPath(insertName);
|
||||
const dbQualifiedLabel = rawDbName
|
||||
? `${rawDbName}.${displayName || rawTableName}`
|
||||
: (displayName || rawTableName);
|
||||
return {
|
||||
displayName: displayName || rawTableName,
|
||||
insertName,
|
||||
insertText,
|
||||
dbQualifiedLabel,
|
||||
};
|
||||
@@ -6072,7 +6158,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
if (threePartMatch) {
|
||||
const dbPart = stripQuotes(threePartMatch[1]);
|
||||
const tablePart = stripQuotes(threePartMatch[2]);
|
||||
const colPrefix = (threePartMatch[3] || '').toLowerCase();
|
||||
const rawColPrefix = String(threePartMatch[3] || '');
|
||||
const colPrefix = rawColPrefix.toLowerCase();
|
||||
|
||||
const cols = await getCompletionColumnsByTable(dbPart, tablePart, dbPart);
|
||||
if (isSqlCompletionRequestCancelled(token)) {
|
||||
@@ -6087,7 +6174,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
buildSuggestion: (column) => ({
|
||||
label: column.name,
|
||||
kind: monaco.languages.CompletionItemKind.Field,
|
||||
insertText: quoteCompletionPart(column.name),
|
||||
insertText: quoteCompletionPart(applyCompletionFragmentCase(column.name, rawColPrefix)),
|
||||
detail: buildColumnCompletionDetail(column),
|
||||
documentation: buildColumnCompletionDocumentation(column),
|
||||
filterText: resolveQueryEditorCompletionFilterText(colPrefix, [column.name]) || column.name,
|
||||
@@ -6102,7 +6189,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const qualifierMatch = linePrefix.match(QUERY_EDITOR_SQL_QUALIFIER_COMPLETION_REGEX);
|
||||
if (qualifierMatch) {
|
||||
const qualifier = stripQuotes(qualifierMatch[1]);
|
||||
const prefix = (qualifierMatch[2] || '').toLowerCase();
|
||||
const rawPrefix = String(qualifierMatch[2] || '');
|
||||
const prefix = rawPrefix.toLowerCase();
|
||||
const qualifierLower = qualifier.toLowerCase();
|
||||
|
||||
// 首先检查 qualifier 是否是数据库名(跨库表提示)
|
||||
@@ -6139,7 +6227,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
[meta.displayName, table.tableName],
|
||||
),
|
||||
kind: monaco.languages.CompletionItemKind.Class,
|
||||
insertText: meta.insertText,
|
||||
insertText: quoteCompletionPath(applyCompletionFragmentCase(meta.insertName, rawPrefix)),
|
||||
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${table.dbName})`, table.comment),
|
||||
documentation: buildCompletionDocumentation(table.comment),
|
||||
range,
|
||||
@@ -6251,7 +6339,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
[parsed.table, table.tableName],
|
||||
),
|
||||
kind: monaco.languages.CompletionItemKind.Class,
|
||||
insertText: quoteCompletionPart(parsed.table),
|
||||
insertText: quoteCompletionPart(applyCompletionFragmentCase(parsed.table, rawPrefix)),
|
||||
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${table.dbName}${parsed.schema ? '.' + parsed.schema : ''})`, table.comment),
|
||||
documentation: buildCompletionDocumentation(table.comment),
|
||||
range,
|
||||
@@ -6355,7 +6443,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
buildSuggestion: (column) => ({
|
||||
label: column.name,
|
||||
kind: monaco.languages.CompletionItemKind.Field,
|
||||
insertText: quoteCompletionPart(column.name),
|
||||
insertText: quoteCompletionPart(applyCompletionFragmentCase(column.name, rawPrefix)),
|
||||
detail: buildColumnCompletionDetail(column),
|
||||
documentation: buildColumnCompletionDocumentation(column),
|
||||
filterText: resolveQueryEditorCompletionFilterText(prefix, [column.name]) || column.name,
|
||||
@@ -6379,7 +6467,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const currentDatabase = getActiveCompletionDbName();
|
||||
const isCurrentCompletionDatabase = (dbName: string) =>
|
||||
String(dbName || '').toLowerCase() === currentDatabase.toLowerCase();
|
||||
const wordPrefix = (word.word || '').toLowerCase();
|
||||
const rawWordPrefix = String(word.word || '');
|
||||
const wordPrefix = rawWordPrefix.toLowerCase();
|
||||
const getPrefixMatchRank = (...candidates: string[]) => {
|
||||
if (!wordPrefix) return '0';
|
||||
const matchRank = rankQueryEditorCompletionCandidate(wordPrefix, candidates);
|
||||
@@ -6502,7 +6591,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return {
|
||||
label: column.name,
|
||||
kind: monaco.languages.CompletionItemKind.Field,
|
||||
insertText: quoteCompletionPart(column.name),
|
||||
insertText: quoteCompletionPart(applyCompletionFragmentCase(column.name, rawWordPrefix)),
|
||||
detail: buildColumnCompletionDetail(column),
|
||||
documentation: buildColumnCompletionDocumentation(column),
|
||||
filterText: resolveQueryEditorCompletionFilterText(wordPrefix, [column.name]) || column.name,
|
||||
@@ -6563,7 +6652,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
[label, table.tableName || '', pureTable],
|
||||
),
|
||||
kind: monaco.languages.CompletionItemKind.Class,
|
||||
insertText: quoteCompletionPath(label),
|
||||
insertText: quoteCompletionPath(applyCompletionFragmentCase(label, rawWordPrefix)),
|
||||
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${table.dbName})`, table.comment),
|
||||
documentation: buildCompletionDocumentation(table.comment),
|
||||
range,
|
||||
@@ -6582,7 +6671,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
[label, table.tableName || '', pureTable],
|
||||
),
|
||||
kind: monaco.languages.CompletionItemKind.Class,
|
||||
insertText: quoteCompletionPath(hasDuplicate ? table.tableName : pureTable),
|
||||
insertText: quoteCompletionPath(applyCompletionFragmentCase(
|
||||
hasDuplicate ? table.tableName : pureTable,
|
||||
rawWordPrefix,
|
||||
)),
|
||||
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')}${schemaInfo}`, table.comment),
|
||||
documentation: buildCompletionDocumentation(table.comment),
|
||||
range,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
resolveInlineSqlInsertText,
|
||||
resolveQueryEditorAiRuntimeReadiness,
|
||||
resolveQueryEditorInlineMemoryInsertText,
|
||||
resolveQueryEditorInlineCompletionEdit,
|
||||
resolveQueryEditorInlineCompletionModel,
|
||||
resolveQueryEditorInlineCompletionIntentDetails,
|
||||
sanitizeSqlAssistantResponse,
|
||||
@@ -173,6 +174,117 @@ describe('QueryEditorAiAssist', () => {
|
||||
})).toBe(' videos SET status = 1 WHERE id = ?;');
|
||||
});
|
||||
|
||||
it('inherits the typed table fragment case without changing the remaining remembered SQL', () => {
|
||||
expect(resolveQueryEditorInlineMemoryInsertText({
|
||||
editorSnapshot: {
|
||||
prefix: 'SELECT * FROM A_C',
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: 'SELECT * FROM A_C',
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
memoryEntries: [
|
||||
{ sql: "SELECT * FROM a_cninfo_announcement where short_title like 'about%'" },
|
||||
],
|
||||
})).toBe("ninfo_announcement where short_title like 'about%'");
|
||||
|
||||
expect(resolveQueryEditorInlineMemoryInsertText({
|
||||
editorSnapshot: {
|
||||
prefix: 'SELECT * FROM a_c',
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: 'SELECT * FROM a_c',
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
memoryEntries: [
|
||||
{ sql: 'SELECT * FROM A_CNINFO_ANNOUNCEMENT WHERE SHORT_TITLE IS NOT NULL' },
|
||||
],
|
||||
})).toBe('ninfo_announcement WHERE SHORT_TITLE IS NOT NULL');
|
||||
});
|
||||
|
||||
it('uses the metadata identifier when accepting case-mismatched table completion', () => {
|
||||
const aiContext = {
|
||||
sourceType: 'mysql',
|
||||
currentDb: 'main',
|
||||
tables: [{ dbName: 'main', tableName: 'a_cninfo_announcement' }],
|
||||
columns: [],
|
||||
};
|
||||
|
||||
expect(resolveQueryEditorInlineCompletionEdit({
|
||||
aiContext,
|
||||
editorSnapshot: {
|
||||
prefix: 'SELECT * FROM A_C',
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: 'SELECT * FROM A_C',
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
insertText: "ninfo_announcement where short_title like 'about%'",
|
||||
})).toEqual({
|
||||
previewText: "ninfo_announcement where short_title like 'about%'",
|
||||
editText: "a_cninfo_announcement where short_title like 'about%'",
|
||||
replacePrefixLength: 3,
|
||||
});
|
||||
|
||||
expect(resolveQueryEditorInlineCompletionEdit({
|
||||
aiContext: {
|
||||
...aiContext,
|
||||
tables: [{ dbName: 'main', tableName: 'TABLE' }],
|
||||
},
|
||||
editorSnapshot: {
|
||||
prefix: 'SELECT * FROM ta',
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: 'SELECT * FROM ta',
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
insertText: 'ble where id = 1',
|
||||
})).toEqual({
|
||||
previewText: 'ble where id = 1',
|
||||
editText: 'table where id = 1',
|
||||
replacePrefixLength: 2,
|
||||
});
|
||||
|
||||
expect(resolveQueryEditorInlineCompletionEdit({
|
||||
aiContext: {
|
||||
...aiContext,
|
||||
currentDb: 'main',
|
||||
tables: [{ dbName: 'analytics', tableName: 'a_cninfo_announcement' }],
|
||||
},
|
||||
editorSnapshot: {
|
||||
prefix: 'SELECT * FROM analytics.A_C',
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: 'SELECT * FROM analytics.A_C',
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
insertText: 'ninfo_announcement',
|
||||
})).toEqual({
|
||||
previewText: 'ninfo_announcement',
|
||||
editText: 'analytics.a_cninfo_announcement',
|
||||
replacePrefixLength: 'analytics.A_C'.length,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves exact-case inline identifiers for PostgreSQL-family dialects', () => {
|
||||
const postgresContext = {
|
||||
sourceType: 'postgres',
|
||||
currentDb: 'main',
|
||||
tables: [{ dbName: 'main', tableName: 'TABLE' }],
|
||||
columns: [],
|
||||
};
|
||||
|
||||
expect(resolveQueryEditorInlineCompletionEdit({
|
||||
aiContext: postgresContext,
|
||||
editorSnapshot: {
|
||||
prefix: 'SELECT * FROM ta',
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: 'SELECT * FROM ta',
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
insertText: 'ble',
|
||||
})).toEqual({
|
||||
previewText: 'BLE',
|
||||
editText: 'TABLE',
|
||||
replacePrefixLength: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('sanitizes fenced SQL and removes duplicated typed prefixes', () => {
|
||||
expect(sanitizeSqlAssistantResponse('```sql\nselect * from users;\n```')).toBe('select * from users;');
|
||||
expect(sanitizeSqlAssistantResponse('SQL: select count(*) from orders;')).toBe('select count(*) from orders;');
|
||||
@@ -613,6 +725,54 @@ describe('QueryEditorAiAssist', () => {
|
||||
expect(service.AIGetActiveProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('inherits the typed fragment case for deterministic table-name completion', async () => {
|
||||
const service = readyService('TABLE');
|
||||
const buildRequest = (fragment: string) => ({
|
||||
service,
|
||||
aiContext: {
|
||||
connectionName: 'Local Dameng',
|
||||
sourceType: 'dameng',
|
||||
currentDb: 'APP',
|
||||
tables: [{ dbName: 'APP', tableName: 'TABLE' }],
|
||||
columns: [],
|
||||
},
|
||||
editorSnapshot: {
|
||||
prefix: `SELECT * FROM ${fragment}`,
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: `SELECT * FROM ${fragment}`,
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(requestQueryEditorInlineCompletion(buildRequest('ta'))).resolves.toBe('ble');
|
||||
await expect(requestQueryEditorInlineCompletion(buildRequest('TA'))).resolves.toBe('BLE');
|
||||
expect(service.AIChatSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('inherits the typed fragment case for deterministic column-name completion', async () => {
|
||||
const service = readyService('SHORT_TITLE');
|
||||
|
||||
const insertText = await requestQueryEditorInlineCompletion({
|
||||
service,
|
||||
aiContext: {
|
||||
connectionName: 'Local Dameng',
|
||||
sourceType: 'dameng',
|
||||
currentDb: 'APP',
|
||||
tables: [{ dbName: 'APP', tableName: 'VIDEOS' }],
|
||||
columns: [{ dbName: 'APP', tableName: 'VIDEOS', name: 'SHORT_TITLE', type: 'varchar' }],
|
||||
},
|
||||
editorSnapshot: {
|
||||
prefix: 'SELECT v.sh FROM VIDEOS v WHERE v.sh',
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: 'SELECT v.sh FROM VIDEOS v WHERE v.sh',
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(insertText).toBe('ort_title');
|
||||
expect(service.AIChatSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses deterministic schema metadata for alter-table inline completion and skips AI', async () => {
|
||||
const service = readyService('ALTER TABLE orders ADD COLUMN status INT;');
|
||||
|
||||
@@ -667,6 +827,33 @@ describe('QueryEditorAiAssist', () => {
|
||||
expect(service.AIChatSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('inherits the typed fragment case for grounded AI table-name completion', async () => {
|
||||
const service = readyService('TABLE');
|
||||
|
||||
const insertText = await requestQueryEditorInlineCompletion({
|
||||
service,
|
||||
aiContext: {
|
||||
connectionName: 'Local Dameng',
|
||||
sourceType: 'dameng',
|
||||
currentDb: 'APP',
|
||||
tables: [
|
||||
{ dbName: 'APP', tableName: 'TABLE' },
|
||||
{ dbName: 'APP', tableName: 'TARGET' },
|
||||
],
|
||||
columns: [],
|
||||
},
|
||||
editorSnapshot: {
|
||||
prefix: 'SELECT * FROM ta',
|
||||
suffix: '',
|
||||
currentLineBeforeCursor: 'SELECT * FROM ta',
|
||||
currentLineAfterCursor: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(insertText).toBe('ble');
|
||||
expect(service.AIChatSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rejects ungrounded AI table-name inline completion when the suggestion is outside schema metadata', async () => {
|
||||
const service = readyService('Japgolly');
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
import {
|
||||
buildQueryEditorAliasMap,
|
||||
} from './QueryEditorHelpers';
|
||||
import { isPostgresSchemaDialect } from '../../utils/connectionDriverType';
|
||||
|
||||
export type QueryEditorAiApplyMode = 'insert' | 'replaceSelection' | 'replaceAll';
|
||||
|
||||
@@ -279,12 +280,53 @@ const normalizeInlineMemoryMatchText = (sql: string): string => (
|
||||
.toLowerCase()
|
||||
);
|
||||
|
||||
export const applyQueryEditorCompletionFragmentCase = (
|
||||
candidate: string,
|
||||
fragment: string,
|
||||
preserveCandidateCase = false,
|
||||
): string => {
|
||||
if (preserveCandidateCase) {
|
||||
return candidate;
|
||||
}
|
||||
const activeFragment = String(fragment || '').trim().split('.').pop() || '';
|
||||
const fragmentCharacters = activeFragment.replace(/[^A-Za-z]/g, '');
|
||||
const candidateParts = String(candidate || '').split('.');
|
||||
const candidateLastPart = candidateParts.pop() || '';
|
||||
const candidateCharacters = candidateLastPart.replace(/[^A-Za-z]/g, '');
|
||||
if (!fragmentCharacters || !candidateCharacters) {
|
||||
return candidate;
|
||||
}
|
||||
// Lowercase input may intentionally target an uppercase metadata name. Do not
|
||||
// uppercase a lowercase metadata name: case-sensitive MySQL deployments would fail.
|
||||
if (
|
||||
fragmentCharacters === fragmentCharacters.toLowerCase()
|
||||
&& candidateCharacters === candidateCharacters.toUpperCase()
|
||||
) {
|
||||
return [...candidateParts, candidateLastPart.toLowerCase()].join('.');
|
||||
}
|
||||
return candidate;
|
||||
};
|
||||
|
||||
const applyInlineMemoryObjectCase = (
|
||||
insertText: string,
|
||||
fragment: string,
|
||||
preserveCandidateCase = false,
|
||||
): string => {
|
||||
const identifierSuffix = String(insertText || '').match(/^[A-Za-z0-9_$]+/)?.[0] || '';
|
||||
if (!identifierSuffix) {
|
||||
return insertText;
|
||||
}
|
||||
return `${applyQueryEditorCompletionFragmentCase(identifierSuffix, fragment, preserveCandidateCase)}${insertText.slice(identifierSuffix.length)}`;
|
||||
};
|
||||
|
||||
export const resolveQueryEditorInlineMemoryInsertText = ({
|
||||
editorSnapshot,
|
||||
memoryEntries,
|
||||
sourceType,
|
||||
}: {
|
||||
editorSnapshot: QueryEditorAiEditorSnapshot;
|
||||
memoryEntries: QueryEditorInlineMemoryEntry[];
|
||||
sourceType?: string;
|
||||
}): string => {
|
||||
if (!shouldAllowQueryEditorInlineMemoryCompletion(editorSnapshot)) {
|
||||
return '';
|
||||
@@ -300,7 +342,13 @@ export const resolveQueryEditorInlineMemoryInsertText = ({
|
||||
if (normalizedStatementPrefix && !normalizeInlineMemoryMatchText(candidateSql).startsWith(normalizedStatementPrefix)) {
|
||||
continue;
|
||||
}
|
||||
return limitInlineInsertText(resolveInlineSqlInsertText(candidateSql, editorSnapshot.prefix));
|
||||
const insertText = resolveInlineSqlInsertText(candidateSql, editorSnapshot.prefix);
|
||||
const intent = resolveQueryEditorInlineCompletionIntentDetails(editorSnapshot);
|
||||
return limitInlineInsertText(
|
||||
intent.intent === 'table_name' || intent.intent === 'column_name'
|
||||
? applyInlineMemoryObjectCase(insertText, intent.fragment, isPostgresSchemaDialect(sourceType || ''))
|
||||
: insertText,
|
||||
);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@@ -1201,15 +1249,22 @@ const filterInlineTableMatches = (
|
||||
): CompletionTableMeta[] => {
|
||||
const normalizedFragment = normalizeInlineIdentifierPath(fragment).toLowerCase();
|
||||
const useQualifiedName = normalizedFragment.includes('.');
|
||||
const matched = collectCurrentDatabaseTables(tables, currentDb).filter((table) => {
|
||||
const sourceTables = useQualifiedName ? tables : collectCurrentDatabaseTables(tables, currentDb);
|
||||
const matched = sourceTables.filter((table) => {
|
||||
if (!normalizedFragment) {
|
||||
return true;
|
||||
}
|
||||
const normalizedTableName = normalizeInlineIdentifierPath(table.tableName || '');
|
||||
const candidate = useQualifiedName
|
||||
? normalizedTableName
|
||||
: getInlineIdentifierLastPart(normalizedTableName);
|
||||
return candidate.toLowerCase().startsWith(normalizedFragment);
|
||||
const normalizedDbName = normalizeInlineIdentifierPath(table.dbName || '');
|
||||
const candidatePaths = useQualifiedName
|
||||
? [
|
||||
normalizedTableName,
|
||||
normalizedDbName && !normalizedTableName.toLowerCase().startsWith(`${normalizedDbName.toLowerCase()}.`)
|
||||
? `${normalizedDbName}.${normalizedTableName}`
|
||||
: '',
|
||||
].filter(Boolean)
|
||||
: [getInlineIdentifierLastPart(normalizedTableName)];
|
||||
return candidatePaths.some((candidate) => candidate.toLowerCase().startsWith(normalizedFragment));
|
||||
});
|
||||
return matched.slice(0, MAX_INLINE_SCHEMA_TABLES);
|
||||
};
|
||||
@@ -1258,6 +1313,7 @@ const resolveInlineColumnOwnerReference = (
|
||||
const resolveUniqueCompletionCandidateInsertText = (
|
||||
candidates: string[],
|
||||
fragment: string,
|
||||
preserveCandidateCase = false,
|
||||
): string => {
|
||||
const dedupedCandidates = Array.from(new Map(
|
||||
candidates
|
||||
@@ -1283,7 +1339,8 @@ const resolveUniqueCompletionCandidateInsertText = (
|
||||
if (prefixMatches.length !== 1) {
|
||||
return '';
|
||||
}
|
||||
return prefixMatches[0].slice(normalizedFragment.length);
|
||||
return applyQueryEditorCompletionFragmentCase(prefixMatches[0], normalizedFragment, preserveCandidateCase)
|
||||
.slice(normalizedFragment.length);
|
||||
};
|
||||
|
||||
const collectInlineTableCandidateLabels = (
|
||||
@@ -1295,7 +1352,19 @@ const collectInlineTableCandidateLabels = (
|
||||
return filterInlineTableMatches(context.tables || [], currentDb, fragment)
|
||||
.map((table) => {
|
||||
const normalizedTableName = normalizeInlineIdentifierPath(table.tableName || '');
|
||||
return useQualifiedName ? normalizedTableName : getInlineIdentifierLastPart(normalizedTableName);
|
||||
if (!useQualifiedName) {
|
||||
return getInlineIdentifierLastPart(normalizedTableName);
|
||||
}
|
||||
const normalizedDbName = normalizeInlineIdentifierPath(table.dbName || '');
|
||||
const candidatePaths = [
|
||||
normalizedTableName,
|
||||
normalizedDbName && !normalizedTableName.toLowerCase().startsWith(`${normalizedDbName.toLowerCase()}.`)
|
||||
? `${normalizedDbName}.${normalizedTableName}`
|
||||
: '',
|
||||
].filter(Boolean);
|
||||
return candidatePaths.find((candidate) => candidate.toLowerCase().startsWith(normalizeInlineIdentifierPath(fragment).toLowerCase()))
|
||||
|| candidatePaths[0]
|
||||
|| '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
@@ -1322,6 +1391,7 @@ const resolveValidatedInlineObjectCandidateInsertText = ({
|
||||
prefix,
|
||||
normalizer,
|
||||
safePattern,
|
||||
preserveCandidateCase = false,
|
||||
}: {
|
||||
candidateLabels: string[];
|
||||
fragment: string;
|
||||
@@ -1329,6 +1399,7 @@ const resolveValidatedInlineObjectCandidateInsertText = ({
|
||||
prefix: string;
|
||||
normalizer: (value: string) => string;
|
||||
safePattern: RegExp;
|
||||
preserveCandidateCase?: boolean;
|
||||
}): string => {
|
||||
const trimmedInsertText = String(insertText || '').trim();
|
||||
if (!trimmedInsertText || !safePattern.test(trimmedInsertText)) {
|
||||
@@ -1354,7 +1425,10 @@ const resolveValidatedInlineObjectCandidateInsertText = ({
|
||||
return '';
|
||||
}
|
||||
|
||||
return resolveInlineSqlInsertText(matchedCandidate, prefix);
|
||||
return resolveInlineSqlInsertText(
|
||||
applyQueryEditorCompletionFragmentCase(matchedCandidate, fragment, preserveCandidateCase),
|
||||
prefix,
|
||||
);
|
||||
};
|
||||
|
||||
const shouldAllowInlineObjectAiFallback = (
|
||||
@@ -1389,7 +1463,11 @@ const resolveDeterministicInlineTableInsertText = (
|
||||
fragment: string,
|
||||
): string => {
|
||||
const candidateLabels = collectInlineTableCandidateLabels(context, fragment);
|
||||
return resolveUniqueCompletionCandidateInsertText(candidateLabels, normalizeInlineIdentifierPath(fragment));
|
||||
return resolveUniqueCompletionCandidateInsertText(
|
||||
candidateLabels,
|
||||
normalizeInlineIdentifierPath(fragment),
|
||||
isPostgresSchemaDialect(context.sourceType || ''),
|
||||
);
|
||||
};
|
||||
|
||||
const resolveDeterministicInlineColumnInsertText = (
|
||||
@@ -1399,7 +1477,11 @@ const resolveDeterministicInlineColumnInsertText = (
|
||||
fragment: string,
|
||||
): string => {
|
||||
const candidateLabels = collectInlineColumnCandidateLabels(context, editorSnapshot, qualifier);
|
||||
return resolveUniqueCompletionCandidateInsertText(candidateLabels, stripInlineIdentifierQuotes(fragment || '').trim());
|
||||
return resolveUniqueCompletionCandidateInsertText(
|
||||
candidateLabels,
|
||||
stripInlineIdentifierQuotes(fragment || '').trim(),
|
||||
isPostgresSchemaDialect(context.sourceType || ''),
|
||||
);
|
||||
};
|
||||
|
||||
const resolveValidatedInlineTableAiInsertText = (
|
||||
@@ -1414,6 +1496,7 @@ const resolveValidatedInlineTableAiInsertText = (
|
||||
prefix: editorSnapshot.prefix,
|
||||
normalizer: normalizeInlineIdentifierPath,
|
||||
safePattern: INLINE_TABLE_FRAGMENT_SAFE_RE,
|
||||
preserveCandidateCase: isPostgresSchemaDialect(context.sourceType || ''),
|
||||
});
|
||||
|
||||
const resolveValidatedInlineColumnAiInsertText = (
|
||||
@@ -1429,6 +1512,7 @@ const resolveValidatedInlineColumnAiInsertText = (
|
||||
prefix: editorSnapshot.prefix,
|
||||
normalizer: stripInlineIdentifierQuotes,
|
||||
safePattern: INLINE_COLUMN_FRAGMENT_SAFE_RE,
|
||||
preserveCandidateCase: isPostgresSchemaDialect(context.sourceType || ''),
|
||||
});
|
||||
|
||||
const shouldAllowInlineTableAiFallback = (
|
||||
@@ -1480,6 +1564,71 @@ const resolveDeterministicInlineSchemaCompletion = (
|
||||
};
|
||||
};
|
||||
|
||||
export interface QueryEditorInlineCompletionEdit {
|
||||
previewText: string;
|
||||
editText: string;
|
||||
replacePrefixLength: number;
|
||||
}
|
||||
|
||||
export const resolveQueryEditorInlineCompletionEdit = ({
|
||||
aiContext,
|
||||
editorSnapshot,
|
||||
insertText,
|
||||
}: {
|
||||
aiContext: QueryEditorAiContext;
|
||||
editorSnapshot: QueryEditorAiEditorSnapshot;
|
||||
insertText: string;
|
||||
}): QueryEditorInlineCompletionEdit => {
|
||||
const fallback: QueryEditorInlineCompletionEdit = {
|
||||
previewText: insertText,
|
||||
editText: insertText,
|
||||
replacePrefixLength: 0,
|
||||
};
|
||||
const intent = resolveQueryEditorInlineCompletionIntentDetails(editorSnapshot);
|
||||
if ((intent.intent !== 'table_name' && intent.intent !== 'column_name') || !intent.fragment) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const candidateLabels = intent.intent === 'table_name'
|
||||
? collectInlineTableCandidateLabels(aiContext, intent.fragment)
|
||||
: collectInlineColumnCandidateLabels(aiContext, editorSnapshot, intent.qualifier);
|
||||
const normalizer = intent.intent === 'table_name'
|
||||
? normalizeInlineIdentifierPath
|
||||
: stripInlineIdentifierQuotes;
|
||||
const token = String(insertText || '').match(/^[A-Za-z0-9_$]+/)?.[0] || '';
|
||||
if (!token) {
|
||||
return fallback;
|
||||
}
|
||||
const normalizedDirectSuggestion = normalizer(token).toLowerCase();
|
||||
const normalizedCombinedSuggestion = normalizer(`${intent.fragment}${token}`).toLowerCase();
|
||||
const matchedCandidate = Array.from(new Map(
|
||||
candidateLabels
|
||||
.map((candidate) => String(candidate || '').trim())
|
||||
.filter(Boolean)
|
||||
.map((candidate) => [normalizer(candidate).toLowerCase(), candidate] as const),
|
||||
).values()).find((candidate) => {
|
||||
const normalizedCandidate = normalizer(candidate).toLowerCase();
|
||||
return normalizedCandidate === normalizedDirectSuggestion
|
||||
|| normalizedCandidate === normalizedCombinedSuggestion;
|
||||
});
|
||||
if (!matchedCandidate) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const canonicalIdentifier = applyQueryEditorCompletionFragmentCase(
|
||||
matchedCandidate,
|
||||
intent.fragment,
|
||||
isPostgresSchemaDialect(aiContext.sourceType || ''),
|
||||
);
|
||||
const remainder = String(insertText).slice(token.length);
|
||||
const editText = `${canonicalIdentifier}${remainder}`;
|
||||
return {
|
||||
previewText: editText.slice(intent.fragment.length),
|
||||
editText,
|
||||
replacePrefixLength: intent.fragment.length,
|
||||
};
|
||||
};
|
||||
|
||||
const buildKeywordSuffixInsertText = (statementPrefix: string, suffix: string): string => (
|
||||
/\s$/.test(statementPrefix) ? suffix : ` ${suffix}`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user