feat(grid): enhance field search and filter interaction

This commit is contained in:
zipg authored and GitHub committed 2026-08-07 13:43:55 +08:00
1 parent 13878a1a30
commit f9e93b356f
26 files changed
+491 -35

No files matched your search

@@ -0,0 +1,82 @@
import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin";
/**
* Scores identifier matches for completion and field pickers.
* Higher scores are better; -1 means there is no match.
*/
export function identifierMatchScore(candidate: string, query: string): number {
if (!query) return 1;
const candidateLower = candidate.toLowerCase();
const queryLower = query.toLowerCase();
if (candidateLower === queryLower) return 3000 - candidateLower.length;
if (candidateLower.startsWith(queryLower)) return 2000 - candidateLower.length;
const initials = identifierInitials(candidate);
if (initials && initials.startsWith(queryLower)) {
const exactInitialsBonus = initials === queryLower ? 400 : 0;
return 2400 + exactInitialsBonus - candidateLower.length;
}
if (/^[a-z0-9]+$/.test(queryLower) && containsHan(candidateLower)) {
const pinyinInitials = pinyinFirstLetters(candidateLower);
if (pinyinInitials.startsWith(queryLower)) {
const exactInitialsBonus = pinyinInitials === queryLower ? 300 : 0;
return 2300 + exactInitialsBonus - candidateLower.length;
}
const subsequence = orderedSubsequenceSpan(pinyinInitials, queryLower);
if (subsequence) {
return 1600 - subsequence.first * 30 - (subsequence.span - queryLower.length) * 10 - candidateLower.length;
}
}
const substringIndex = candidateLower.indexOf(queryLower);
if (substringIndex >= 0) {
const boundaryBonus = isIdentifierBoundary(candidate, substringIndex) ? 400 : Math.max(0, 180 - substringIndex * 12);
return 900 + boundaryBonus - candidateLower.length;
}
let candidateIndex = 0;
let totalGap = 0;
let firstMatchPosition = -1;
let boundaryBonus = 0;
for (const character of queryLower) {
const nextPosition = candidateLower.indexOf(character, candidateIndex);
if (nextPosition === -1) return -1;
if (firstMatchPosition === -1) firstMatchPosition = nextPosition;
if (isIdentifierBoundary(candidate, nextPosition)) boundaryBonus += 40;
totalGap += nextPosition - candidateIndex;
candidateIndex = nextPosition + 1;
}
const earlyMatchBonus = Math.max(0, 700 - firstMatchPosition * 35) + boundaryBonus;
if (totalGap >= queryLower.length) {
return 400 + earlyMatchBonus * 0.3 - totalGap * 20 - candidateLower.length;
}
return 1200 + earlyMatchBonus - totalGap * 10 - candidateLower.length;
}
export function matchesIdentifierSearch(candidate: string, query: string): boolean {
return identifierMatchScore(candidate, query.trim()) >= 0;
}
function identifierWords(candidate: string): string[] {
return candidate
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter(Boolean);
}
function identifierInitials(candidate: string): string {
return identifierWords(candidate)
.map((part) => part[0])
.join("");
}
function isIdentifierBoundary(candidate: string, index: number): boolean {
if (index <= 0) return true;
const previous = candidate[index - 1] ?? "";
const current = candidate[index] ?? "";
return /[^A-Za-z0-9]/.test(previous) || (/[a-z0-9]/.test(previous) && /[A-Z]/.test(current));
}
+6 -4
View File
@@ -10,6 +10,7 @@ import { findActiveSqlStatementSpan, tokenizeSqlSemantic } from "@/lib/sql/seman
import type { SqlSemanticBuildOptions, SqlSemanticSpan } from "@/lib/sql/semantic/types";
import { DEFAULT_SQL_SNIPPETS, MANTICORESEARCH_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
import { requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier";
import { identifierMatchScore, matchesIdentifierSearch } from "@/lib/sql/identifierSearch";
import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin";
import { quoteTableIdentifier } from "@/lib/table/tableSelectSql";
@@ -3635,16 +3636,17 @@ function buildColumnItems(context: SqlCompletionContext, columnsByTable: Map<str
const relevanceBoost = context.referencedTables.length > 0 || !!context.qualifier || !!context.insertTable ? 2000 : 0;
return uniqueColumns
.filter((column) => matchesPrefix(column.displayLabel, context.prefix))
.filter((column) => matchesIdentifierSearch(column.name, context.prefix) || matchesIdentifierSearch(column.displayLabel, context.prefix))
.map((column) => {
const keyBoost = isKeyColumn(column.name) ? 500 : 0;
const matchScore = Math.max(identifierMatchScore(column.name, context.prefix), identifierMatchScore(column.displayLabel, context.prefix));
return {
label: column.displayLabel,
type: "column" as const,
detail: buildColumnDetail(column),
info: buildColumnInfo(column),
apply: buildColumnApply(column, context, dialect),
boost: computeBoost(column.displayLabel, context.prefix) + keyBoost + relevanceBoost,
boost: matchScore + keyBoost + relevanceBoost,
};
})
.sort(compareCompletionItems);
@@ -3667,7 +3669,7 @@ function applyReferencedColumnAliases<T extends SqlCompletionColumn>(table: SqlC
function hasMatchingReferencedColumnPrefix(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>): boolean {
if (!context.suggestColumns || !context.prefix || context.referencedTables.length === 0) return false;
return context.referencedTables.some((table) => columnsForReferencedTable(table, columnsByTable).some((column) => matchesPrefix(column.name, context.prefix)));
return context.referencedTables.some((table) => columnsForReferencedTable(table, columnsByTable).some((column) => matchesIdentifierSearch(column.name, context.prefix)));
}
function qualifiedTableTargetFromContext(context: SqlCompletionContext): { database?: string; schema: string; table: string } | null {
@@ -4230,7 +4232,7 @@ function buildNonAggregatedColumnItems(context: SqlCompletionContext, columnsByT
for (const col of cols) {
const key = col.name.toLowerCase();
if (!nonAggSet.has(key) || seen.has(key)) continue;
if (context.prefix && !matchesPrefix(col.name, context.prefix)) continue;
if (context.prefix && !matchesIdentifierSearch(col.name, context.prefix)) continue;
seen.add(key);
items.push({
label: col.name,