feat(query-editor): 优化表候选框展示与注释 (#807)

## 背景
Issue #778 中,SQL 编辑器的数据表候选框无法稳定展示完整表名和表注释。

## 变更
- 表候选使用 Monaco 结构化标签固定双行展示,第一行保留表名,第二行展示表类型、数据库/Schema 和注释。
- 注释使用灰色小字号右对齐,并保留表名过滤、前缀补全和原有插入文本。
- 仅在查询编辑器的候选组件范围内增加样式,不改变其它页面。

## 影响范围
仅影响 SQL 查询编辑器的数据表候选框展示;候选排序、过滤、键盘选择和 SQL 插入逻辑保持不变。

## 验证
- `npx tsc --noEmit`
- `npx vitest run
src/components/queryEditor/QueryEditorHelpers.test.ts`(24/24)
- `npx vite build`(7966 modules transformed)
- `git diff --check`

Closes #778
This commit is contained in:
Syngnat
2026-08-01 10:59:42 +08:00
committed by GitHub
2 changed files with 148 additions and 7 deletions

View File

@@ -294,6 +294,97 @@ body[data-theme='light'] ::-webkit-scrollbar-thumb:hover {
white-space: nowrap;
}
/* 表候选使用 Monaco 原生 CompletionItemLabel第一行表名第二行注释。 */
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) {
height: 36px !important;
min-height: 36px !important;
max-height: 36px !important;
}
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents {
height: 36px !important;
overflow: visible !important;
}
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main {
display: grid !important;
grid-template-columns: 18px minmax(0, 1fr);
grid-template-rows: 18px 18px;
grid-template-areas:
"icon name"
"comment comment";
align-content: center;
gap: 0 !important;
height: 36px;
line-height: 18px !important;
overflow: visible;
white-space: normal;
}
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .icon-label.codicon {
grid-area: icon;
width: 18px;
height: 18px;
align-self: center;
}
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .left {
grid-area: name;
display: flex;
align-items: center;
width: 100%;
height: 18px;
min-width: 0;
max-width: none;
overflow: visible;
line-height: 18px !important;
}
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .left > .monaco-icon-label {
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .right {
grid-area: comment;
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
height: 18px;
min-width: 0;
max-width: none;
overflow: visible;
line-height: 18px !important;
}
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .right > .details-label {
display: block !important;
width: max-content;
min-width: 0;
max-width: none;
flex: 0 0 auto;
margin-left: 0 !important;
overflow: visible;
color: var(--vscode-editorSuggestWidget-foreground);
font-size: 11px !important;
line-height: 18px !important;
opacity: 0.68;
text-align: right;
white-space: nowrap;
}
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused:not(.string-label) > .contents > .main > .right > .details-label {
color: var(--vscode-editorSuggestWidget-selectedForeground);
}
.gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .right > .readMore {
display: none !important;
}
/* Ensure body background matches theme to avoid white flashes, but kept transparent for window composition */
body {
transition: color 0.3s;

View File

@@ -396,6 +396,7 @@ const buildQueryEditorMonacoOptions = (
scrollBeyondLastLine: false,
quickSuggestions: { other: true, comments: false, strings: false },
suggestOnTriggerCharacters: true,
suggestLineHeight: QUERY_EDITOR_TABLE_SUGGESTION_ROW_HEIGHT,
inlineSuggest: buildQueryEditorAiInlineSuggestOptions(),
...(isObjectEditQueryTab
? {
@@ -992,6 +993,28 @@ let sharedConnections: any[] = [];
let sharedTablesData: CompletionTableMeta[] = [];
let sharedAllColumnsData: CompletionColumnMeta[] = [];
const QUERY_EDITOR_TABLE_SUGGESTION_ROW_HEIGHT = 36;
const normalizeQueryEditorTableSuggestionText = (value: unknown): string => (
String(value ?? '').replace(/\r\n|\r|\n/g, '').trim()
);
const buildQueryEditorTableSuggestionLabel = (
label: unknown,
description?: unknown,
useStructuredLabel = true,
): any => {
const normalizedLabel = normalizeQueryEditorTableSuggestionText(label);
const normalizedDescription = normalizeQueryEditorTableSuggestionText(description);
if (!useStructuredLabel) {
return normalizedLabel;
}
return {
label: normalizedLabel,
description: normalizedDescription,
};
};
// AI 补全的元数据预热可能把整库列(数十万条)灌入 sharedAllColumnsData普通补全逐列全量
// 扫描会阻塞主线程;按 (库, 表名末段) 建索引,并以数组身份为键缓存,数组重新赋值时自动失效。
const sharedColumnsIndexCache = new WeakMap<CompletionColumnMeta[], Map<string, CompletionColumnMeta[]>>();
@@ -3876,6 +3899,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const handleEditorDidMount: OnMount = (editor, monaco) => {
editorRef.current = editor;
monacoRef.current = monaco;
// CompletionItemLabel is rendered by Monaco's DOM suggest widget. Keep
// the original string label for non-DOM adapters used by older hosts.
const useStructuredCompletionLabel = typeof editor?.getDomNode?.()?.querySelector === 'function';
const suggestController = editor.getContribution?.('editor.contrib.suggestController') as {
widget?: { value?: { _details?: { widget?: { layout?: (width: number, height: number) => void } } } };
@@ -5185,6 +5211,14 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
dbQualifiedLabel,
};
};
const buildTableSuggestion = (label: string, prefix: string, comment?: string) => ({
label: buildQueryEditorTableSuggestionLabel(
label,
appendCommentToDetail(prefix, comment),
useStructuredCompletionLabel,
),
filterText: normalizeQueryEditorTableSuggestionText(label),
});
const normalizeRoutineType = (routineType: string) => (
String(routineType || '').trim().toUpperCase().includes('PROC') ? 'PROCEDURE' : 'FUNCTION'
);
@@ -5523,7 +5557,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return createEmptySqlCompletionResult();
}
}
const tableBatch = createBoundedQueryEditorCompletionCandidateBatch({
const tableBatch = createBoundedQueryEditorCompletionCandidateBatch<CompletionTableMeta, any>({
candidates: tables,
prefix,
getMatchRank: (table, normalizedPrefix) => {
@@ -5538,7 +5572,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
buildSuggestion: (table) => {
const meta = buildDbQualifiedTableSuggestionMeta(table.dbName || qualifier, table.tableName || '');
return {
label: meta.displayName,
...buildTableSuggestion(
meta.displayName,
`${translate('query_editor.object_info.table')} (${table.dbName})`,
table.comment,
),
kind: monaco.languages.CompletionItemKind.Class,
insertText: meta.insertText,
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${table.dbName})`, table.comment),
@@ -5627,7 +5665,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
// qualifier 是 schema如 dbo/public仅补全表名避免输入 dbo. 后再补成 dbo.dbo.table
let hasKnownSchemaQualifier = false;
const schemaTableBatch = createBoundedQueryEditorCompletionCandidateBatch({
const schemaTableBatch = createBoundedQueryEditorCompletionCandidateBatch<CompletionTableMeta, any>({
candidates: sharedTablesData,
prefix,
getMatchRank: (table, normalizedPrefix) => {
@@ -5641,7 +5679,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
buildSuggestion: (table) => {
const parsed = splitSchemaAndTable(table.tableName || '');
return {
label: parsed.table,
...buildTableSuggestion(
parsed.table,
`${translate('query_editor.object_info.table')} (${table.dbName}${parsed.schema ? '.' + parsed.schema : ''})`,
table.comment,
),
kind: monaco.languages.CompletionItemKind.Class,
insertText: quoteCompletionPart(parsed.table),
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${table.dbName}${parsed.schema ? '.' + parsed.schema : ''})`, table.comment),
@@ -5911,7 +5953,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
: [];
const tableNameToSchemaCount = getCompletionTableSchemaCounts(currentDatabaseTables);
const tableBatch = createBoundedQueryEditorCompletionCandidateBatch({
const tableBatch = createBoundedQueryEditorCompletionCandidateBatch<CompletionTableMeta, any>({
candidates: completionTables,
prefix: wordPrefix,
getMatchRank: (table, normalizedPrefix) => {
@@ -5946,7 +5988,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const meta = buildDbQualifiedTableSuggestionMeta(table.dbName || '', table.tableName || '');
const label = meta.dbQualifiedLabel;
return {
label,
...buildTableSuggestion(
label,
`${translate('query_editor.object_info.table')} (${table.dbName})`,
table.comment,
),
kind: monaco.languages.CompletionItemKind.Class,
insertText: quoteCompletionPath(label),
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${table.dbName})`, table.comment),
@@ -5959,7 +6005,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const label = hasDuplicate ? table.tableName : pureTable;
const schemaInfo = parsed.schema ? ` (${parsed.schema})` : '';
return {
label,
...buildTableSuggestion(
label,
`${translate('query_editor.object_info.table')}${schemaInfo}`,
table.comment,
),
kind: monaco.languages.CompletionItemKind.Class,
insertText: quoteCompletionPath(hasDuplicate ? table.tableName : pureTable),
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')}${schemaInfo}`, table.comment),