一、SQL 智能补全功能
现代数据库管理工具中,SQL 智能补全功能已成为提升开发效率的重要特性。它能够根据上下文环境自动提示关键词、表名、字段名等信息,大大减少了手动输入错误和查询文档的时间。这种智能化的代码辅助功能不仅提高了编写 SQL 的准确性,还能帮助开发者快速回忆表结构和语法规范。本文将介绍如何使用 HTML、CSS 和 JavaScript 实现 SQL 智能补全功能。
二、效果演示
这个 SQL 智能补全功能提供了一个直观的编辑环境,当用户在文本框中输入 SQL 语句时,系统会根据上下文智能推荐关键词、表名和字段名。例如,当用户在 SELECT 关键词后输入时,系统会推荐可用的字段名;在 FROM 后面则会推荐数据库中的表名。补全建议会以下拉菜单的形式显示,用户可以通过键盘方向键导航,按 Enter 或 Tab 键确认选择,也可以点击鼠标直接选择。

三、系统分析
1.页面结构
页面主要包括以下几个区域:
1.1 SQL 编辑器区域
这是主要的输入区域,用户在这里编写 SQL 语句。
html
<div class="sql-editor-wrapper">
<textarea id="sqlEditor" placeholder="输入SQL语句,体验智能补全功能..."></textarea>
<div class="autocomplete-dropdown" id="autocompleteDropdown"></div>
</div>
1.2 信息面板区域
底部的信息面板提供了使用说明和快捷键提示。
html
<div class="info-panel">
支持:关键词、表名、字段名智能补全 | 快捷键:↑↓选择,Enter/Tab确认,Esc取消 | 表:user, order
</div>
2.核心功能实现
2.1 SQL 词汇表构建
智能补全功能的基础是完整的词汇表,包括 SQL 关键词和预定义的表结构。代码首先定义了常用 SQL 关键词数组 SQL_KEYWORDS,包含 SELECT、FROM、WHERE 等标准 SQL 语句关键词。同时,通过 TABLES 对象定义了两个示例表(user 和 order)及其字段,这些数据构成了智能补全的数据基础。为了提高查询效率,代码创建了缓存数据结构:TABLE_NAMES 存储所有表名,ALL_COLUMNS 是所有字段的集合,而 COLUMNS_BY_TABLE 则建立了表到其字段的映射关系。
javascript
// SQL关键词
const SQL_KEYWORDS = [
'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE', 'SET',
'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'OUTER JOIN',
// ... 其他关键词
];
// 表结构定义
const TABLES = {
user: { columns: ['id', 'username', 'email', 'password', 'created_at', 'updated_at', 'status', 'nickname', 'avatar'] },
order: { columns: ['id', 'user_id', 'order_no', 'amount', 'status', 'created_at', 'pay_time', 'remark', 'payment_method'] }
};
// 缓存数据
const TABLE_NAMES = Object.keys(TABLES);
const ALL_COLUMNS = new Set();
const COLUMNS_BY_TABLE = {};
TABLE_NAMES.forEach(table => {
COLUMNS_BY_TABLE[table] = new Set(TABLES[table].columns);
TABLES[table].columns.forEach(col => ALL_COLUMNS.add(col));
});
2.2 令牌提取与上下文分析
智能补全的关键在于准确识别用户当前正在输入的内容。
getTokenContext 函数负责从编辑器中提取光标前的文本,并使用正则表达式匹配当前正在输入的令牌(token)。这个函数返回令牌本身、令牌起始位置和光标位置等信息,为后续的上下文分析提供基础。
getContext 函数进一步分析当前的 SQL 上下文,确定用户可能需要的补全类型。它通过检查光标前的文本模式来判断当前处于什么 SQL 语句部分:如果前面是 FROM、JOIN 或 INTO,则认为是表名上下文;如果是 ON,则可能是列名且涉及表别名;如果是 WHERE、ORDER BY 等,则是列名上下文;如果检测到表别名(如 "t." 形式),则提供该表的字段建议。
javascript
function getTokenContext() {
const cursorPos = editor.selectionStart;
const text = editor.value;
const beforeCursor = text.substring(0, cursorPos);
const tokenMatch = beforeCursor.match(/[\w.]*$/);
const token = tokenMatch ? tokenMatch[0] : '';
const tokenStart = cursorPos - token.length;
return { token, tokenStart, cursorPos, text };
}
function getContext(text, cursorPos, tokenStart) {
const beforeToken = text.substring(0, tokenStart).toUpperCase().trim();
if (/FROM\s*$/.test(beforeToken) || /JOIN\s*$/.test(beforeToken) || /INTO\s*$/.test(beforeToken)) {
return { type: 'table', prefix: '' };
}
if (/ON\s*$/.test(beforeToken)) {
return { type: 'column', prefix: '', tableContext: true };
}
if (/(WHERE|ORDER BY|GROUP BY|HAVING|SELECT|SET)\s*$/.test(beforeToken)) {
return { type: 'column', prefix: '' };
}
const aliasMatch = text.substring(Math.max(0, tokenStart - 50), tokenStart).match(/(\w+)\.$/);
if (aliasMatch) {
const alias = aliasMatch[1].toLowerCase();
if (COLUMNS_BY_TABLE[alias]) {
return { type: 'column', prefix: '', table: alias };
}
}
return { type: 'mixed', prefix: '' };
}
2.3 智能建议算法
getSuggestions 函数实现了建议算法,它根据上下文类型返回相应的补全选项。算法首先定义了 calculateScore 函数来计算匹配度分数,该函数考虑了多种因素:完全匹配得分最高,前缀匹配次之,包含匹配再次,最后根据字符串长度进行微调。基于上下文类型,算法分别处理表名、字段名和混合类型的建议。对于表名上下文,只返回匹配的表名;对于字段名上下文,如果没有指定表则返回所有字段,如果有指定表则只返回该表的字段;对于混合上下文,则同时考虑关键词、表名和字段名。所有建议按分数排序并限制数量。
javascript
function getSuggestions(token, context) {
const suggestions = [];
const tokenLower = token.toLowerCase();
const cleanToken = token.replace(/[`"]/g, '');
const cleanTokenLower = cleanToken.toLowerCase();
function calculateScore(text, search, typePriority) {
let score = typePriority * 100;
const textLower = text.toLowerCase();
const searchLower = search.toLowerCase();
if (textLower === searchLower) {
score += 1000;
} else if (textLower.startsWith(searchLower)) {
score += 500;
} else if (textLower.includes(searchLower)) {
score += 200;
}
score -= text.length * 0.1;
return score;
}
if (context.type === 'table') {
TABLE_NAMES.forEach(table => {
if (table.toLowerCase().includes(cleanTokenLower)) {
const score = calculateScore(table, cleanToken, 10);
suggestions.push({ text: table, type: 'table', score });
}
});
} else if (context.type === 'column') {
// ... 字段上下文,搜索字段名
} else {
// ... 混合上下文,搜索关键词、表名和字段名
}
return suggestions.sort((a, b) => b.score - a.score).slice(0, 12);
}
2.4 下拉框定位与渲染
为了提供良好的用户体验,下拉框需要精确地出现在光标附近。
calculateDropdownPosition 函数计算下拉框的绝对位置,它首先通过 textMeasurer 元素测量当前行的文本宽度,然后计算光标所在的行列位置,从而确定下拉框的左上角坐标。
renderDropdown 函数负责创建和显示下拉框。它首先清空现有内容,然后为每个建议创建 autocomplete-item 元素,包含建议文本和类型标识。每个项目都绑定点击事件和鼠标悬停事件,使用户可以通过鼠标或键盘选择建议。
javascript
function calculateDropdownPosition(tokenStart) {
const text = editor.value;
const beforeToken = text.substring(0, tokenStart);
const lines = beforeToken.split('\n');
const currentLineText = lines[lines.length - 1];
const editorRect = editor.getBoundingClientRect();
const editorStyle = window.getComputedStyle(editor);
const paddingLeft = parseFloat(editorStyle.paddingLeft);
const paddingTop = parseFloat(editorStyle.paddingTop);
const paddingRight = parseFloat(editorStyle.paddingRight);
const lineHeight = parseFloat(editorStyle.lineHeight) || 21;
textMeasurer.textContent = currentLineText;
const textWidth = textMeasurer.offsetWidth;
const left = paddingLeft + textWidth;
const top = paddingTop + (lines.length - 1) * lineHeight;
const dropdownHeight = Math.min(200, dropdown.scrollHeight || 150);
const dropdownWidth = 220;
const spaceBelow = editorRect.height - top;
const displayAbove = spaceBelow < dropdownHeight + 10;
const adjustedLeft = Math.min(left, editorRect.width - dropdownWidth - paddingRight - 5);
return {
left: adjustedLeft,
top: displayAbove ? top - dropdownHeight - 5 : top + lineHeight,
displayAbove
};
}
function renderDropdown(suggestions, tokenStart) {
if (suggestions.length === 0) {
hideDropdown();
return;
}
currentSuggestions = suggestions;
selectedIndex = -1;
dropdown.innerHTML = '';
suggestions.forEach((suggestion, index) => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
item.setAttribute('data-index', index);
const text = suggestion.displayText || suggestion.text;
item.innerHTML = `<span>${text}</span><span class="autocomplete-type">${suggestion.type}</span>`;
item.addEventListener('click', () => selectSuggestion(index));
item.addEventListener('mouseenter', () => setSelectedIndex(index));
dropdown.appendChild(item);
});
const position = calculateDropdownPosition(tokenStart);
dropdown.style.left = position.left + 'px';
dropdown.style.top = position.top + 'px';
dropdown.style.display = 'block';
}
2.5 交互控制与事件处理
智能补全功能的交互体验依赖于完善的事件处理机制。本项目中实现了多种事件监听器来处理用户输入、键盘导航等。
input 事件在用户输入时触发建议生成。为了避免频繁更新影响性能,代码使用防抖技术,延迟 80 毫秒执行建议生成逻辑。如果输入的令牌长度大于等于 1,则根据上下文获取建议并渲染下拉框;否则隐藏下拉框。
keydown 事件处理键盘导航:方向键用于在建议项间移动选择,Enter 和 Tab 键确认选择,Esc 键取消建议。
javascript
editor.addEventListener('input', () => {
if (compositionInProgress) return;
clearTimeout(inputDebounceTimer);
inputDebounceTimer = setTimeout(() => {
const { token, tokenStart, cursorPos } = getTokenContext();
if (token.length >= 1) {
const context = getContext(editor.value, cursorPos, tokenStart);
const suggestions = getSuggestions(token, context);
renderDropdown(suggestions, tokenStart);
} else {
hideDropdown();
}
}, 80);
});
editor.addEventListener('keydown', (e) => {
if (compositionInProgress) return;
if (dropdown.style.display === 'block') {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex(Math.min(selectedIndex + 1, currentSuggestions.length - 1));
return;
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex(Math.max(selectedIndex - 1, -1));
return;
} else if (e.key === 'Enter' || e.key === 'Tab') {
if (currentSuggestions.length > 0) {
e.preventDefault();
selectSuggestion(Math.max(selectedIndex, 0));
return;
}
} else if (e.key === 'Escape') {
e.preventDefault();
hideDropdown();
return;
}
}
if ([' ', '(', ')', ',', ';', '.', '[', ']'].includes(e.key)) {
setTimeout(() => {
const { token } = getTokenContext();
if (token.length < 1) {
hideDropdown();
}
}, 0);
}
});
四、扩展建议
- 添加数据库连接功能,动态获取真实表结构信息
- 实现语法高亮,提升代码可读性和编辑体验
- 增加一键美化功能,自动调整缩进、换行使代码结构清晰
- 增加历史记录功能,方便用户查看之前的查询语句
- 支持更多数据库方言,适应不同的数据库系统
五、完整代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SQL智能补全功能</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.header {
background: #fafafa;
border-bottom: 1px solid #e0e0e0;
padding: 12px 15px;
font-size: 14px;
color: #333;
font-weight: 500;
}
.sql-editor-wrapper {
position: relative;
}
#sqlEditor {
width: 100%;
min-height: 400px;
border: none;
outline: none;
padding: 15px;
font-size: 14px;
line-height: 1.5;
resize: vertical;
background: #fff;
color: #333;
tab-size: 4;
}
.autocomplete-dropdown {
position: absolute;
background: #fff;
border: 1px solid #d0d0d0;
border-radius: 3px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
display: none;
z-index: 1000;
min-width: 220px;
font-size: 13px;
font-family: inherit;
}
.autocomplete-item {
padding: 7px 12px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
color: #333;
border-bottom: 1px solid #f5f5f5;
transition: background 0.1s;
}
.autocomplete-item:last-child {
border-bottom: none;
}
.autocomplete-item:hover,
.autocomplete-item.active {
background: #f0f0f0;
}
.autocomplete-type {
font-size: 11px;
color: #666;
background: #e8e8e8;
padding: 2px 6px;
border-radius: 2px;
margin-left: 10px;
text-transform: uppercase;
}
.info-panel {
background: #fafafa;
border-top: 1px solid #e0e0e0;
padding: 12px 15px;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">SQL 编辑器 - 智能补全</div>
<div class="sql-editor-wrapper">
<textarea id="sqlEditor" placeholder="输入SQL语句,体验智能补全功能..."></textarea>
<div class="autocomplete-dropdown" id="autocompleteDropdown"></div>
</div>
<div class="info-panel">
支持:关键词、表名、字段名智能补全 | 快捷键:↑↓选择,Enter/Tab确认,Esc取消 | 表:user, order
</div>
</div>
<script>
// SQL关键词
const SQL_KEYWORDS = [
'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE', 'SET',
'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'OUTER JOIN',
'ON', 'AS', 'DISTINCT', 'GROUP BY', 'ORDER BY', 'HAVING',
'LIMIT', 'OFFSET', 'UNION', 'ALL', 'EXISTS', 'IN', 'NOT IN',
'BETWEEN', 'LIKE', 'ILIKE', 'AND', 'OR', 'NOT', 'NULL',
'IS', 'COUNT', 'SUM', 'AVG', 'MAX', 'MIN', 'CREATE', 'TABLE',
'INDEX', 'PRIMARY KEY', 'FOREIGN KEY', 'REFERENCES', 'ALTER',
'DROP', 'VIEW', 'TRIGGER', 'PROCEDURE', 'FUNCTION', 'DATABASE',
'SCHEMA', 'INT', 'VARCHAR', 'TEXT', 'DATE', 'DATETIME', 'BOOLEAN',
'DECIMAL', 'FLOAT', 'DOUBLE', 'CHAR', 'BIGINT', 'SMALLINT'
];
// 表结构定义
const TABLES = {
user: { columns: ['id', 'username', 'email', 'password', 'created_at', 'updated_at', 'status', 'nickname', 'avatar'] },
order: { columns: ['id', 'user_id', 'order_no', 'amount', 'status', 'created_at', 'pay_time', 'remark', 'payment_method'] }
};
// 缓存数据
const TABLE_NAMES = Object.keys(TABLES);
const ALL_COLUMNS = new Set();
const COLUMNS_BY_TABLE = {};
TABLE_NAMES.forEach(table => {
COLUMNS_BY_TABLE[table] = new Set(TABLES[table].columns);
TABLES[table].columns.forEach(col => ALL_COLUMNS.add(col));
});
// DOM元素
const editor = document.getElementById('sqlEditor');
const dropdown = document.getElementById('autocompleteDropdown');
const wrapper = document.querySelector('.sql-editor-wrapper');
// 状态变量
let currentSuggestions = [];
let selectedIndex = -1;
let compositionInProgress = false;
let inputDebounceTimer;
// 创建一个隐藏的元素来测量文本宽度
const textMeasurer = document.createElement('span');
textMeasurer.style.position = 'absolute';
textMeasurer.style.visibility = 'hidden';
textMeasurer.style.whiteSpace = 'pre';
textMeasurer.style.font = getComputedStyle(editor).font;
document.body.appendChild(textMeasurer);
// 获取当前token和位置
function getTokenContext() {
const cursorPos = editor.selectionStart;
const text = editor.value;
const beforeCursor = text.substring(0, cursorPos);
const tokenMatch = beforeCursor.match(/[\w.]*$/);
const token = tokenMatch ? tokenMatch[0] : '';
const tokenStart = cursorPos - token.length;
return { token, tokenStart, cursorPos, text };
}
// 智能上下文分析
function getContext(text, cursorPos, tokenStart) {
const beforeToken = text.substring(0, tokenStart).toUpperCase().trim();
if (/FROM\s*$/.test(beforeToken) || /JOIN\s*$/.test(beforeToken) || /INTO\s*$/.test(beforeToken)) {
return { type: 'table', prefix: '' };
}
if (/ON\s*$/.test(beforeToken)) {
return { type: 'column', prefix: '', tableContext: true };
}
if (/(WHERE|ORDER BY|GROUP BY|HAVING|SELECT|SET)\s*$/.test(beforeToken)) {
return { type: 'column', prefix: '' };
}
const aliasMatch = text.substring(Math.max(0, tokenStart - 50), tokenStart).match(/(\w+)\.$/);
if (aliasMatch) {
const alias = aliasMatch[1].toLowerCase();
if (COLUMNS_BY_TABLE[alias]) {
return { type: 'column', prefix: '', table: alias };
}
}
return { type: 'mixed', prefix: '' };
}
// 获取补全建议
function getSuggestions(token, context) {
const suggestions = [];
const tokenLower = token.toLowerCase();
const cleanToken = token.replace(/[`"]/g, '');
const cleanTokenLower = cleanToken.toLowerCase();
function calculateScore(text, search, typePriority) {
let score = typePriority * 100;
const textLower = text.toLowerCase();
const searchLower = search.toLowerCase();
if (textLower === searchLower) {
score += 1000;
} else if (textLower.startsWith(searchLower)) {
score += 500;
} else if (textLower.includes(searchLower)) {
score += 200;
}
score -= text.length * 0.1;
return score;
}
if (context.type === 'table') {
TABLE_NAMES.forEach(table => {
if (table.toLowerCase().includes(cleanTokenLower)) {
const score = calculateScore(table, cleanToken, 10);
suggestions.push({ text: table, type: 'table', score });
}
});
} else if (context.type === 'column') {
if (context.table) {
TABLES[context.table].columns.forEach(col => {
if (col.toLowerCase().includes(cleanTokenLower)) {
const score = calculateScore(col, cleanToken, 20);
suggestions.push({
text: col,
type: 'column',
displayText: col,
score
});
}
});
} else {
ALL_COLUMNS.forEach(col => {
if (col.toLowerCase().includes(cleanTokenLower)) {
const score = calculateScore(col, cleanToken, 8);
suggestions.push({
text: col,
type: 'column',
score
});
}
});
TABLE_NAMES.forEach(table => {
TABLES[table].columns.forEach(col => {
if (col.toLowerCase().includes(cleanTokenLower)) {
const score = calculateScore(col, cleanToken, 3);
suggestions.push({
text: `${table}.${col}`,
type: 'column',
displayText: `${table}.${col}`,
score
});
}
});
});
}
} else {
SQL_KEYWORDS.forEach(keyword => {
if (keyword.toLowerCase().includes(tokenLower)) {
const score = calculateScore(keyword, token, 5);
suggestions.push({ text: keyword, type: 'keyword', score });
}
});
TABLE_NAMES.forEach(table => {
if (table.toLowerCase().includes(tokenLower)) {
const score = calculateScore(table, token, 12);
suggestions.push({ text: table, type: 'table', score });
}
});
ALL_COLUMNS.forEach(col => {
if (col.toLowerCase().includes(tokenLower)) {
const score = calculateScore(col, token, 7);
suggestions.push({
text: col,
type: 'column',
score
});
}
});
}
return suggestions.sort((a, b) => b.score - a.score).slice(0, 12);
}
// 计算下拉框位置
function calculateDropdownPosition(tokenStart) {
const text = editor.value;
const beforeToken = text.substring(0, tokenStart);
const lines = beforeToken.split('\n');
const currentLineText = lines[lines.length - 1];
const editorRect = editor.getBoundingClientRect();
const editorStyle = window.getComputedStyle(editor);
const paddingLeft = parseFloat(editorStyle.paddingLeft);
const paddingTop = parseFloat(editorStyle.paddingTop);
const paddingRight = parseFloat(editorStyle.paddingRight);
const lineHeight = parseFloat(editorStyle.lineHeight) || 21;
textMeasurer.textContent = currentLineText;
const textWidth = textMeasurer.offsetWidth;
const left = paddingLeft + textWidth;
const top = paddingTop + (lines.length - 1) * lineHeight;
const dropdownHeight = Math.min(200, dropdown.scrollHeight || 150);
const dropdownWidth = 220;
const spaceBelow = editorRect.height - top;
const displayAbove = spaceBelow < dropdownHeight + 10;
const adjustedLeft = Math.min(left, editorRect.width - dropdownWidth - paddingRight - 5);
return {
left: adjustedLeft,
top: displayAbove ? top - dropdownHeight - 5 : top + lineHeight,
displayAbove
};
}
// 渲染下拉框
function renderDropdown(suggestions, tokenStart) {
if (suggestions.length === 0) {
hideDropdown();
return;
}
currentSuggestions = suggestions;
selectedIndex = -1;
dropdown.innerHTML = '';
suggestions.forEach((suggestion, index) => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
item.setAttribute('data-index', index);
const text = suggestion.displayText || suggestion.text;
item.innerHTML = `<span>${text}</span><span class="autocomplete-type">${suggestion.type}</span>`;
item.addEventListener('click', () => selectSuggestion(index));
item.addEventListener('mouseenter', () => setSelectedIndex(index));
dropdown.appendChild(item);
});
const position = calculateDropdownPosition(tokenStart);
dropdown.style.left = position.left + 'px';
dropdown.style.top = position.top + 'px';
dropdown.style.display = 'block';
}
// 选择建议
function selectSuggestion(index) {
if (index < 0 || index >= currentSuggestions.length) return;
const suggestion = currentSuggestions[index];
const { token, tokenStart, cursorPos } = getTokenContext();
const beforeToken = editor.value.substring(0, tokenStart);
const afterToken = editor.value.substring(cursorPos);
let insertText = suggestion.text;
editor.value = beforeToken + insertText + afterToken;
const newCursorPos = tokenStart + insertText.length;
editor.setSelectionRange(newCursorPos, newCursorPos);
hideDropdown();
editor.focus();
}
// 隐藏下拉框
function hideDropdown() {
dropdown.style.display = 'none';
currentSuggestions = [];
selectedIndex = -1;
}
// 设置选中索引
function setSelectedIndex(index) {
const items = dropdown.querySelectorAll('.autocomplete-item');
items.forEach(item => item.classList.remove('active'));
if (index >= 0 && index < items.length) {
items[index].classList.add('active');
items[index].scrollIntoView({ block: 'nearest' });
}
selectedIndex = index;
}
// 键盘事件处理
editor.addEventListener('keydown', (e) => {
if (compositionInProgress) return;
if (dropdown.style.display === 'block') {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex(Math.min(selectedIndex + 1, currentSuggestions.length - 1));
return;
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex(Math.max(selectedIndex - 1, -1));
return;
} else if (e.key === 'Enter' || e.key === 'Tab') {
if (currentSuggestions.length > 0) {
e.preventDefault();
selectSuggestion(Math.max(selectedIndex, 0));
return;
}
} else if (e.key === 'Escape') {
e.preventDefault();
hideDropdown();
return;
}
}
if ([' ', '(', ')', ',', ';', '.', '[', ']'].includes(e.key)) {
setTimeout(() => {
const { token } = getTokenContext();
if (token.length < 1) {
hideDropdown();
}
}, 0);
}
});
// 输入事件处理
editor.addEventListener('input', () => {
if (compositionInProgress) return;
clearTimeout(inputDebounceTimer);
inputDebounceTimer = setTimeout(() => {
const { token, tokenStart, cursorPos } = getTokenContext();
if (token.length >= 1) {
const context = getContext(editor.value, cursorPos, tokenStart);
const suggestions = getSuggestions(token, context);
renderDropdown(suggestions, tokenStart);
} else {
hideDropdown();
}
}, 80);
});
// 中文输入法处理
editor.addEventListener('compositionstart', () => {
compositionInProgress = true;
hideDropdown();
});
editor.addEventListener('compositionend', () => {
compositionInProgress = false;
editor.dispatchEvent(new Event('input'));
});
// 点击外部隐藏下拉框
document.addEventListener('mousedown', (e) => {
if (!e.target.closest('.sql-editor-wrapper')) {
hideDropdown();
}
});
// 其他事件监听
editor.addEventListener('scroll', hideDropdown);
window.addEventListener('resize', hideDropdown);
editor.addEventListener('paste', () => {
setTimeout(() => editor.dispatchEvent(new Event('input')), 0);
});
editor.addEventListener('blur', () => {
setTimeout(() => {
if (!document.activeElement.closest('.autocomplete-dropdown')) {
hideDropdown();
}
}, 200);
});
</script>
</body>
</html>