第九章:辅助功能系统
本章介绍 VSDB 的辅助功能,包括查询历史、书签系统、全局搜索和错误处理。
9.1 查询历史系统
HistoryManager 设计
graph TD
A["执行查询"] --> B["HistoryManager.add()"]
B --> C["添加到历史列表"]
C --> D["检查上限"]
D -->|"超过 100 条"| E["清理未固定项"]
D -->|"未超过"| F["保存到 globalState"]
G["查看历史"] --> H["TreeProvider"]
H --> I["显示历史节点"]
I --> J["点击重用"]
J --> K["填充到 SQL Editor"]
实现细节
export class HistoryManager {
private maxItems = 100;
constructor(private globalState: vscode.Memento) {}
// 添加历史项
add(item: QueryHistoryItem): void {
const history = this.getList();
history.unshift(item); // 新项在前
// 清理策略:先清理未固定,超过上限时清理最旧的
while (history.length > this.maxItems) {
// 从末尾找未固定项
for (let i = history.length - 1; i >= 0; i--) {
if (!history[i].pinned) {
history.splice(i, 1);
break;
}
}
// 全部固定时,清理最旧的固定项
if (history.length > this.maxItems) {
history.pop();
}
}
this.globalState.update('queryHistory', history);
}
// 固定历史项
pin(id: string): void {
const history = this.getList();
const item = history.find(h => h.id === id);
if (item) {
item.pinned = true;
this.globalState.update('queryHistory', history);
}
}
// 取消固定
unpin(id: string): void {
const history = this.getList();
const item = history.find(h => h.id === id);
if (item) {
item.pinned = false;
this.globalState.update('queryHistory', history);
}
}
// 删除单项
remove(id: string): void {
const history = this.getList().filter(h => h.id !== id);
this.globalState.update('queryHistory', history);
}
// 清空未固定
clear(): void {
const pinned = this.getList().filter(h => h.pinned);
this.globalState.update('queryHistory', pinned);
}
// 获取列表
getList(limit?: number): QueryHistoryItem[] {
const history = this.globalState.get<QueryHistoryItem[]>('queryHistory', []);
return limit ? history.slice(0, limit) : history;
}
}
TreeView 历史节点
class HistoryNode extends vscode.TreeItem {
constructor(public item: QueryHistoryItem) {
super(item.sql.substring(0, 50) + '...', vscode.TreeItemCollapsibleState.None);
this.contextValue = item.pinned ? 'history-pinned' : 'history';
this.iconPath = item.pinned
? new vscode.ThemeIcon('pin')
: new vscode.ThemeIcon('history');
this.tooltip = `${item.connectionName}\n${item.sql}\nExecuted: ${item.executedAt}`;
this.description = `${item.executionTime}ms, ${item.rowCount || 0} rows`;
}
}
9.2 书签系统
BookmarkManager 设计
graph TD
A["添加书签"] --> B["BookmarkManager.add()"]
B --> C["保存到 globalState"]
D["查看书签"] --> E["TreeProvider"]
E --> F["显示书签节点"]
F --> G["展开分组"]
H["管理书签"] --> I["移动到分组"]
H --> J["删除书签"]
H --> K["创建分组"]
实现细节
export class BookmarkManager {
constructor(private globalState: vscode.Memento) {}
add(bookmark: Bookmark): void {
const bookmarks = this.getList();
bookmark.id = uuid();
bookmark.createdAt = new Date();
bookmarks.push(bookmark);
this.globalState.update('bookmarks', bookmarks);
}
remove(id: string): void {
const bookmarks = this.getList().filter(b => b.id !== id);
this.globalState.update('bookmarks', bookmarks);
}
getList(): Bookmark[] {
return this.globalState.get<Bookmark[]>('bookmarks', []);
}
// 分组管理
getGroups(): BookmarkGroup[] {
return this.globalState.get<BookmarkGroup[]>('bookmarkGroups', []);
}
createGroup(name: string, color?: string): BookmarkGroup {
const groups = this.getGroups();
const group: BookmarkGroup = {
id: uuid(),
name,
color,
};
groups.push(group);
this.globalState.update('bookmarkGroups', groups);
return group;
}
moveToGroup(bookmarkId: string, groupId?: string): void {
const bookmarks = this.getList();
const bookmark = bookmarks.find(b => b.id === bookmarkId);
if (bookmark) {
bookmark.groupId = groupId;
this.globalState.update('bookmarks', bookmarks);
}
}
// 按分组获取书签
getByGroup(groupId?: string): Bookmark[] {
return this.getList().filter(b => b.groupId === groupId);
}
}
书签节点类型
// 书签分组节点
class BookmarkGroupNode extends vscode.TreeItem {
constructor(public group: BookmarkGroup) {
super(group.name, vscode.TreeItemCollapsibleState.Collapsed);
this.contextValue = 'bookmark-group';
this.iconPath = new vscode.ThemeIcon('folder', new vscode.ThemeColor(group.color));
}
}
// 书签项节点
class BookmarkNode extends vscode.TreeItem {
constructor(public bookmark: Bookmark) {
super(bookmark.name, vscode.TreeItemCollapsibleState.None);
this.contextValue = `bookmark-${bookmark.type}`;
// 根据类型设置图标
switch (bookmark.type) {
case 'connection':
this.iconPath = new vscode.ThemeIcon('plug');
break;
case 'table':
this.iconPath = new vscode.ThemeIcon('table');
break;
case 'query':
this.iconPath = new vscode.ThemeIcon('file-code');
break;
}
this.tooltip = `Type: ${bookmark.type}`;
}
}
9.3 全局搜索
SearchEngine 设计
graph TD
A["用户输入关键词"] --> B["SearchEngine.searchAll()"]
B --> C["遍历活动连接"]
C --> D["searchTables()"]
C --> E["searchColumns()"]
C --> F["searchData()"]
D --> G["查询 Schema Cache"]
E --> G
F --> H["生成 SELECT LIKE SQL"]
G --> I["匹配过滤"]
H --> I
I --> J["合并结果"]
J --> K["返回 SearchResult[]"]
实现细节
export class SearchEngine {
private schemaCache = new Map<string, SchemaCacheEntry>();
private cacheTimeout = 60000; // 1 分钟缓存
async searchAll(keyword: string, options: SearchOptions = {}): Promise<SearchResult[]> {
const results: SearchResult[] = [];
const connectionIds = options.connectionIds || await this.getActiveConnectionIds();
for (const connectionId of connectionIds) {
if (options.searchTables !== false) {
results.push(...await this.searchTables(connectionId, keyword, options));
}
if (options.searchColumns !== false) {
results.push(...await this.searchColumns(connectionId, keyword, options));
}
if (options.searchData) {
results.push(...await this.searchData(connectionId, keyword, options));
}
}
return results;
}
async searchTables(
connectionId: string,
keyword: string,
options: SearchOptions
): Promise<SearchResult[]> {
const schema = await this.getSchemaWithCache(connectionId);
const matcher = this.createMatcher(keyword, options.caseSensitive);
return schema.tables
.filter(t => matcher(t.name))
.map(t => ({
type: 'table',
connectionId,
connectionName: await this.getConnectionName(connectionId),
database: t.schema || 'public',
name: t.name,
detail: `${t.rowCount || '?'} rows`,
}));
}
async searchData(
connectionId: string,
keyword: string,
options: SearchOptions
): Promise<SearchResult[]> {
const results: SearchResult[] = [];
const tables = options.tables || await this.getAllTables(connectionId);
for (const table of tables) {
// 生成搜索 SQL
const columns = await this.getTableColumns(connectionId, table);
const whereClause = columns
.map(c => `${c.name} LIKE '%${keyword}%'`)
.join(' OR ');
const sql = `SELECT * FROM ${table} WHERE ${whereClause} LIMIT 100`;
try {
const response = await this.ipcManager.sendRequest({
type: 'query',
connectionId,
payload: { sql },
});
if (response.data?.rowCount > 0) {
results.push({
type: 'data',
connectionId,
connectionName: await this.getConnectionName(connectionId),
database: '',
name: keyword,
detail: `Found in ${table}: ${response.data.rowCount} rows`,
});
}
} catch {
// 搜索失败跳过
}
}
return results;
}
private createMatcher(keyword: string, caseSensitive?: boolean): (value: string) => boolean {
const k = caseSensitive ? keyword : keyword.toLowerCase();
return (value: string) => {
const v = caseSensitive ? value : value.toLowerCase();
return v.includes(k);
};
}
private async getSchemaWithCache(connectionId: string): Promise<SchemaCacheEntry> {
const cached = this.schemaCache.get(connectionId);
if (cached && Date.now() - cached.cachedAt.getTime() < this.cacheTimeout) {
return cached;
}
// 刷新缓存
const tables = await this.fetchTables(connectionId);
const columns = await this.fetchColumns(connectionId);
const entry: SchemaCacheEntry = {
tables,
columns,
cachedAt: new Date(),
};
this.schemaCache.set(connectionId, entry);
return entry;
}
clearCache(): void {
this.schemaCache.clear();
}
}
搜索命令
const searchCmd = vscode.commands.registerCommand(COMMANDS.SEARCH, async () => {
const keyword = await vscode.window.showInputBox({
prompt: 'Search tables, columns, or data',
placeHolder: 'Enter search keyword...',
});
if (!keyword) return;
vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: `VSDB: Searching for "${keyword}"...`,
}, async () => {
const results = await searchEngine!.searchAll(keyword, {
searchTables: true,
searchColumns: true,
searchData: false, // 数据搜索较慢,默认关闭
});
if (results.length === 0) {
vscode.window.showInformationMessage(`No results found for "${keyword}"`);
return;
}
const items = results.map(r => ({
label: r.name,
description: r.detail || '',
detail: `${r.connectionName} / ${r.database}`,
result: r,
}));
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `Found ${results.length} results`,
matchOnDescription: true,
matchOnDetail: true,
});
if (selected) {
// 导航到结果
navigateToResult(selected.result);
}
});
});
9.4 错误处理系统
ErrorHandler 设计
export class ErrorHandler {
private outputChannel: vscode.OutputChannel;
constructor(outputChannel: vscode.OutputChannel) {
this.outputChannel = outputChannel;
}
handleError(error: Error | unknown, context?: string): void {
const message = this.extractMessage(error);
const code = this.extractCode(error);
// 记录到输出通道
this.outputChannel.appendLine(
`[${new Date().toISOString()}] ERROR: ${context || ''} ${message}`
);
// 根据错误类型决定是否显示用户通知
if (this.shouldNotifyUser(error)) {
vscode.window.showErrorMessage(this.toUserMessage(error));
}
}
extractMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
return String(error);
}
extractCode(error: unknown): string {
if (error && typeof error === 'object' && 'code' in error) {
return String((error as any).code);
}
return 'UNKNOWN';
}
shouldNotifyUser(error: unknown): boolean {
const message = this.extractMessage(error);
// 连接相关错误显示通知
if (message.includes('connect') ||
message.includes('ECONNREFUSED') ||
message.includes('timeout')) {
return true;
}
// Worker 崩溃显示通知
if (message.includes('Worker') || message.includes('crash')) {
return true;
}
return false;
}
toUserMessage(error: unknown): string {
const message = this.extractMessage(error);
// 简化技术细节
if (message.includes('ECONNREFUSED')) {
return 'VSDB: Connection refused. Check if database is running.';
}
if (message.includes('ETIMEDOUT')) {
return 'VSDB: Connection timed out. Check network connectivity.';
}
if (message.includes('syntax error')) {
return 'VSDB: SQL syntax error.';
}
if (message.includes('Access denied')) {
return 'VSDB: Access denied. Check username/password.';
}
return `VSDB: ${message}`;
}
}
Worker 崩溃回调
// extension.ts
const ipcManager = new IpcManager({
workerScriptPath: workerPath,
restartOnCrash: true,
callbacks: {
onWorkerCrash: (attempt: number) => {
errorHandler?.handleError(new Error(`Worker crashed, restart attempt ${attempt}`));
vscode.window.showWarningMessage(
`VSDB: Worker process crashed. Restarting (attempt ${attempt})...`
);
},
onWorkerRestarted: () => {
vscode.window.showInformationMessage('VSDB: Worker process restarted successfully');
},
onConnectionsLost: (connectionIds: string[]) => {
errorHandler?.handleError(new Error(`Connections lost: ${connectionIds.join(', ')}`));
treeProvider?.refresh(); // 更新连接状态显示
},
},
});
9.5 小结
本章介绍了 VSDB 的辅助功能系统:
| 功能 | 实现模块 | 关键特性 |
|---|---|---|
| 查询历史 | HistoryManager | 上限 100 条,固定防清理 |
| 书签系统 | BookmarkManager | 分组管理,多种书签类型 |
| 全局搜索 | SearchEngine | Schema 缓存,表/列/数据搜索 |
| 错误处理 | ErrorHandler | 输出通道记录,用户友好提示 |
关键设计:
- 历史清理策略:优先清理未固定项
- 书签分组:支持创建分组、移动书签
- 搜索缓存:Schema 缓存 1 分钟提升性能
- 错误友好化:技术错误转换为用户友好提示
下一章将介绍开发与调试流程。