mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-07 23:23:52 +08:00
✨ 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:
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user