Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第九章:辅助功能系统

本章介绍 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分组管理,多种书签类型
全局搜索SearchEngineSchema 缓存,表/列/数据搜索
错误处理ErrorHandler输出通道记录,用户友好提示

关键设计:

  • 历史清理策略:优先清理未固定项
  • 书签分组:支持创建分组、移动书签
  • 搜索缓存:Schema 缓存 1 分钟提升性能
  • 错误友好化:技术错误转换为用户友好提示

下一章将介绍开发与调试流程。