🐛 fix(query-editor): 修复 SQL 片段补全与插入交互

- 插入 SQL 片段后主动关闭片段弹窗,保持右键插入行为一致
- 提升 Monaco 补全详情区最小高度,放大片段说明展示区域
- 列补全解析改为基于当前语句引用上下文,修复当前语句前半段补全丢列问题
- 为 SQL 草稿持久化补齐 window 定时器兜底,避免测试环境 clearTimeout 缺失
- 更新 SQL 片段弹窗关闭和补全说明高度相关回归测试
This commit is contained in:
Syngnat
2026-07-01 18:11:41 +08:00
parent 04a61a50dc
commit faaf7169da
4 changed files with 74 additions and 12 deletions

View File

@@ -3642,7 +3642,7 @@ describe('QueryEditor external SQL save', () => {
})],
);
expect(editorState.value).toBe('SELECT id FROM user_table;');
expect(renderer.root.findByProps({ 'data-query-editor-snippet-picker': 'true' })).toBeTruthy();
expect(renderer.root.findAllByProps({ 'data-query-editor-snippet-picker': 'true' })).toHaveLength(0);
});
it('prefers Monaco snippet controller insertion when the controller is available', async () => {
@@ -3695,7 +3695,7 @@ describe('QueryEditor external SQL save', () => {
);
expect(editorState.editor.executeEdits).not.toHaveBeenCalled();
expect(editorState.value).toBe('ALTER TABLE demo_table\nADD COLUMN user_name VARCHAR(255);');
expect(renderer.root.findByProps({ 'data-query-editor-snippet-picker': 'true' })).toBeTruthy();
expect(renderer.root.findAllByProps({ 'data-query-editor-snippet-picker': 'true' })).toHaveLength(0);
});
it('keeps the SQL snippet picker modal non-mask-closable to avoid immediate close after context-menu click', () => {
@@ -8892,6 +8892,24 @@ describe('QueryEditor external SQL save', () => {
expect(css).not.toContain('body[data-ui-version="v2"] .gn-v2-query-monaco-stage .monaco-editor .find-widget {');
});
it('raises QueryEditor suggest docs height for SQL snippet completion without widening global Monaco defaults', () => {
const appCss = readFileSync(new URL('../App.css', import.meta.url), 'utf8');
expect(queryEditorSource).toContain('QUERY_EDITOR_SQL_SNIPPET_SUGGEST_DETAIL_MIN_HEIGHT = 260');
expect(queryEditorSource).toContain("editor.getContribution?.('editor.contrib.suggestController')");
expect(queryEditorSource).toContain('const originalSuggestDetailsLayout = suggestDetailsWidget.layout.bind(suggestDetailsWidget);');
expect(queryEditorSource).toContain('suggestDetailsWidget.layout = (width: number, height: number) => {');
expect(queryEditorSource).toContain('Math.max(height, QUERY_EDITOR_SQL_SNIPPET_SUGGEST_DETAIL_MIN_HEIGHT)');
expect(queryEditorSource).toContain("className={isV2Ui ? 'gn-v2-query-monaco-stage gn-query-monaco-stage' : 'gn-query-monaco-stage'}");
expect(appCss).toContain('.gn-query-monaco-stage .monaco-editor .suggest-details-container {');
expect(appCss).toContain('min-height: 260px;');
expect(appCss).toContain('.gn-query-monaco-stage .monaco-editor .suggest-details {');
expect(appCss).toContain('min-height: 260px;');
expect(appCss).not.toContain('.gn-query-monaco-stage .monaco-editor .suggest-widget {');
expect(appCss).not.toContain('width: 680px;');
expect(appCss).not.toContain('min-width: 560px;');
});
it('keeps the v2 query editor toolbar grouped and compact', () => {
const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');

View File

@@ -742,6 +742,7 @@ const clearRecord = (record: Record<string, unknown>) => {
delete record[key];
});
};
const QUERY_EDITOR_SQL_SNIPPET_SUGGEST_DETAIL_MIN_HEIGHT = 260;
const buildSqlSnippetVariableMap = (now: Date): Record<string, string> => {
const pad = (value: number) => String(value).padStart(2, '0');
@@ -1197,6 +1198,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
if (typeof nextValue === 'string') {
applyQueryState(nextValue);
}
handleCloseSqlSnippetPicker();
editor.focus?.();
return;
}
@@ -1246,8 +1248,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
if (typeof nextValue === 'string') {
applyQueryState(nextValue);
}
handleCloseSqlSnippetPicker();
editor.focus?.();
}, [applyQueryState]);
}, [applyQueryState, handleCloseSqlSnippetPicker]);
useEffect(() => {
persistQueryTabDraftSnapshot(draftSnapshotTab, query, {
@@ -2600,6 +2603,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const handleEditorDidMount: OnMount = (editor, monaco) => {
editorRef.current = editor;
monacoRef.current = monaco;
const suggestController = editor.getContribution?.('editor.contrib.suggestController') as {
widget?: { value?: { _details?: { widget?: { layout?: (width: number, height: number) => void } } } };
} | null;
const suggestDetailsWidget = suggestController?.widget?.value?._details?.widget;
if (suggestDetailsWidget?.layout) {
const originalSuggestDetailsLayout = suggestDetailsWidget.layout.bind(suggestDetailsWidget);
suggestDetailsWidget.layout = (width: number, height: number) => {
originalSuggestDetailsLayout(width, Math.max(height, QUERY_EDITOR_SQL_SNIPPET_SUGGEST_DETAIL_MIN_HEIGHT));
};
}
lastEditorCursorPositionRef.current = normalizeEditorPosition(editor.getPosition?.());
editor.updateOptions?.({
@@ -3415,6 +3429,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
? fullText.slice(currentStatementRange.start, cursorOffset)
: fullText.slice(0, cursorOffset);
const completionScopeText = currentStatementPrefix || linePrefix;
const currentStatementText = currentStatementRange?.text || '';
const completionReferenceText = currentStatementText || completionScopeText;
// 0) 三段式 db.table.column 格式:当输入 db.table. 时提示列
const threePartMatch = linePrefix.match(QUERY_EDITOR_SQL_THREE_PART_COMPLETION_REGEX);
@@ -3552,7 +3568,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
// 否则检查是否是表别名或表名,提示列
const aliasMap = buildQueryEditorAliasMap(completionScopeText, sharedCurrentDb || '');
const aliasMap = buildQueryEditorAliasMap(completionReferenceText, sharedCurrentDb || '');
const tableInfo = aliasMap[qualifier.toLowerCase()];
if (tableInfo) {
@@ -3586,7 +3602,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
tableRegex.lastIndex = 0;
const foundTables = new Set<string>();
let match;
while ((match = tableRegex.exec(completionScopeText)) !== null) {
while ((match = tableRegex.exec(completionReferenceText)) !== null) {
const t = normalizeQualifiedName(match[1] || '');
if (!t) continue;
// 存储完整标识 db.table 或 table
@@ -3685,7 +3701,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const referencedColumns: CompletionColumnMeta[] = [];
if (!expectsTableName) {
const aliasMapForReferencedTables = buildQueryEditorAliasMap(completionScopeText, currentDatabase);
const aliasMapForReferencedTables = buildQueryEditorAliasMap(completionReferenceText, currentDatabase);
const seenReferencedTables = new Set<string>();
for (const tableInfo of Object.values(aliasMapForReferencedTables)) {
const key = `${String(tableInfo.dbName || '').toLowerCase()}.${String(tableInfo.tableName || '').toLowerCase()}`;
@@ -6223,7 +6239,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
<div
ref={editorStageRef}
className={isV2Ui ? 'gn-v2-query-monaco-stage' : undefined}
className={isV2Ui ? 'gn-v2-query-monaco-stage gn-query-monaco-stage' : 'gn-query-monaco-stage'}
style={resolvedQueryEditorStageStyle}
>
<div