1. 项目概述:一个为开发者量身定制的代码统计工具

如果你和我一样,每天大部分时间都泡在 Cursor 编辑器里,看着满屏的代码文件,有没有那么一瞬间,会好奇自己到底写了多少行代码?或者,在接手一个新项目时,想快速了解项目的代码规模和文件构成,却不想去运行那些复杂的命令行工具?我最近就遇到了这个需求,并且发现了一个非常有意思的开源项目: Dwtexe/cursor-stats

简单来说, cursor-stats 是一个专门为 Cursor 编辑器设计的插件或工具,它的核心功能就是帮你快速、直观地统计当前工作区(Workspace)或特定文件夹的代码信息。它不是一个独立的桌面应用,而更像是一个集成在 Cursor 内部的“仪表盘”,让你无需离开编辑器,就能对代码量、文件类型分布、甚至可能是提交历史有一个清晰的概览。这对于个人开发者回顾工作成果,或是团队 leader 快速评估项目复杂度,都提供了极大的便利。

这个工具的出现,正好击中了开发者在日常编码中的一个痒点:我们渴望数据驱动的洞察,但又讨厌繁琐的流程。传统的代码行数统计(比如用 cloc 命令)功能强大,但需要切换到终端、输入命令、解析输出,步骤不少。而 cursor-stats 的目标,就是把这个过程无缝嵌入到你的开发环境中,实现“一键统计,所见即所得”。

2. 核心功能与设计思路拆解

2.1 核心需求:为什么我们需要一个编辑器内的统计工具?

在深入技术细节之前,我们先聊聊为什么这样一个工具是有价值的。从我十多年的开发经验来看,代码统计远不止是数数行数那么简单,它背后关联着几个非常实际的需求:

需求一:项目复杂度快速评估。 当你打开一个陌生的项目仓库,第一眼除了看 README,就是想知道这个项目有多大。有多少个文件?主要是什么语言写的? cursor-stats 能立刻给你一个直观的饼图或列表,告诉你这个项目包含 45% 的 TypeScript,30% 的 CSS,剩下的是一些配置和文档。这比你去猜或者一个个文件夹点开要高效得多。

需求二:个人或团队的工作量化与回顾。 对于自由职业者或者需要写工作周报的开发者来说,一周写了多少行代码、修复了多少个文件,是很有价值的参考数据。虽然代码行数不能完全代表产出质量,但它是一个客观的、可量化的指标。 cursor-stats 如果支持按时间筛选(比如统计过去7天的变更),那价值就更大了。

需求三:代码质量管理的辅助。 通过统计不同文件类型的分布,可以间接发现一些问题。例如,如果一个前端项目里 JavaScript 文件行数远超 TypeScript,可能意味着代码类型安全有待提升;如果测试文件( *.spec.js *.test.ts )的占比过低,可能提示测试覆盖率不足。

需求四:满足开发者的“数据好奇心”。 纯粹的好奇心驱动。我就经常想知道,我维护的那个核心模块,历经三年迭代,现在到底有多少行了? cursor-stats 提供了一种轻量级、低成本的满足这种好奇心的方式。

cursor-stats 的设计思路正是围绕这些需求展开的。它没有选择做一个功能大而全的独立应用,而是精准地切入 “在编辑器内快速可视化” 这个场景,通过降低使用门槛(无需命令行、配置简单)来提升使用频率,从而让代码统计从一项偶尔执行的“任务”,变成一种随时可用的“洞察”。

2.2 技术方案选型:如何与 Cursor 编辑器深度集成?

既然定位是 Cursor 编辑器的专属工具,那么技术栈的选择就必须围绕 Cursor 的扩展能力来展开。Cursor 本身基于 VS Code,因此其插件生态与 VS Code 兼容。这意味着 cursor-stats 极有可能是一个 VS Code/Cursor 扩展 (Extension)。

为什么选择开发扩展?

  1. 原生集成体验 :扩展可以拥有自己的侧边栏视图、状态栏项、命令面板入口,与编辑器 UI 无缝融合,体验最好。
  2. 完整的 API 支持 :VS Code 提供了丰富的扩展 API,可以访问工作区文件列表、监听文件变化、执行命令、显示 WebView 等,这是实现统计功能的基础。
  3. 分发与安装便捷 :开发完成后,可以发布到 VS Code 扩展市场,用户一键即可安装,更新也方便。

核心实现技术点推测:

  • 语言 :大概率是 TypeScript 。这是 VS Code 扩展开发的首选和官方推荐语言,类型系统对构建复杂插件非常友好。
  • 前端视图 :统计结果需要可视化展示。扩展可以使用 VS Code 提供的 Webview API 来渲染一个自定义的 HTML/CSS/JS 界面,用于显示图表(如使用 Chart.js, D3.js 或更轻量的库)和列表。另一种更轻量的方式是使用 TreeDataProvider 在侧边栏展示一个树形列表,但图表能力较弱。
  • 文件遍历与统计引擎 :这是核心逻辑。扩展需要利用 vscode.workspace.findFiles API 来获取工作区内的文件列表,然后根据文件后缀进行过滤和分类。统计代码行数时,需要读取文件内容。这里要注意性能,对于大型项目(如 node_modules),必须进行路径忽略。逻辑本身可以用 Node.js 的原生 fs 模块配合一些字符串处理来实现。
  • 配置与持久化 :用户可能需要忽略某些文件夹(如 dist , .git , node_modules )或文件类型。扩展需要提供配置项,并利用 VS Code 的 WorkspaceConfiguration ExtensionContext.globalState workspaceState 来保存用户设置和缓存统计结果,以提升二次打开的速度。

注意 :这里描述的是一种基于 VS Code 扩展模式的、最合理和常见的实现方式。由于 Dwtexe/cursor-stats 是一个开源项目,其具体实现可能略有不同,但整体技术方向应该是一致的。

3. 核心功能模块深度解析

3.1 工作区扫描与文件过滤机制

任何统计工具的第一步都是获取数据源。 cursor-stats 需要精准、高效地扫描整个工作区。

1. 使用 VS Code API 进行文件发现: 扩展不会直接用 Node.js 的 fs.readdir 递归遍历,而是优先使用 vscode.workspace.findFiles 这个 API。这是一个异步方法,它内部使用了 Cursor/VS Code 的文件系统提供程序,能更好地处理符号链接、远程文件系统(如 SSH, WSL, Docker)等复杂情况。基本调用方式如下:

import * as vscode from 'vscode';
// 查找所有文件,但排除 .git 和 node_modules 等常见目录
const files = await vscode.workspace.findFiles('**/*', '**/{.git,node_modules,dist,build}/**');

这里的 **/* 是 Glob 模式,表示匹配所有文件。第二个参数是排除模式。

2. 可配置的忽略模式: 一个健壮的工具必须允许用户自定义忽略规则。 cursor-stats 应该在扩展的配置项( package.json 中的 contributes.configuration )中定义如 cursorStats.ignorePatterns 的配置。 用户可以在 settings.json 中这样设置:

{
  "cursorStats.ignorePatterns": [
    "**/.git/**",
    "**/node_modules/**",
    "**/*.min.js",
    "**/coverage/**",
    "**/dist/**",
    "**/.next/**",
    "**/out/**"
  ]
}

扫描时,工具需要将用户自定义的忽略模式与内置的默认模式合并,应用到 findFiles 的 exclude 参数中。

3. 按文件类型分类: 获取到文件 URI 列表后,需要根据文件后缀名进行分类。这里需要一个预定义的“语言-后缀”映射表。例如:

const languageMap: {[ext: string]: string} = {
  '.js': 'JavaScript',
  '.ts': 'TypeScript',
  '.jsx': 'React (JS)',
  '.tsx': 'React (TS)',
  '.vue': 'Vue',
  '.py': 'Python',
  '.java': 'Java',
  '.go': 'Go',
  '.rs': 'Rust',
  '.css': 'CSS',
  '.scss': 'SCSS',
  '.less': 'Less',
  '.html': 'HTML',
  '.json': 'JSON',
  '.md': 'Markdown',
  // ... 其他更多
};

对于没有匹配到的后缀,可以归类为“其他”或直接显示后缀名。分类的目的是为后续的图表可视化准备数据。

3.2 代码行数统计的逻辑与细节

统计代码行数是核心中的核心。但“行数”的定义有很多种,工具需要做出明确的选择。

1. 统计什么?

  • 物理行数 (Physical Lines) :就是文件的总行数,包括空行和注释。这是最简单的统计方式,用 content.split('\n').length 即可得到。但它可能“水分”较大。
  • 逻辑行数/代码行数 (Lines of Code, LOC) :通常指排除空行和注释后的行数。这更能反映“有效代码”的量,但计算更复杂。
  • 源代码行数 (Source Lines of Code, SLOC) :与 LOC 类似,但定义可能因工具而异。

对于 cursor-stats 这类轻量级可视化工具,我推测它更可能采用 物理行数 。原因如下:

  • 实现简单,性能高 :不需要为每种语言编写复杂的注释解析器。
  • 结果稳定 :无论什么语言,物理行数的定义都是一致的。
  • 目标匹配 :其目标主要是给开发者一个宏观的、直观的规模感受,而非精确的工程度量。物理行数足以满足这个需求。

2. 性能优化:大文件处理与增量更新 一次性读取和统计成百上千个文件,可能会阻塞编辑器。必须进行优化:

  • 异步流式处理 :使用 Promise.all p-limit 这类库限制并发数,分批读取文件,避免一次性打开太多文件描述符。
  • 大文件跳过或采样 :对于超过一定大小(如 1MB)的文本文件,可以提示用户或跳过统计,因为巨大的日志文件、压缩后的数据文件等不应计入代码统计。
  • 缓存机制 :如果文件没有变化(通过监听 vscode.workspace.onDidSaveTextDocument 或比较文件哈希),可以直接使用上一次的统计结果,极大提升重复统计的速度。
  • 进度反馈 :在统计过程中,应该在状态栏或 Webview 中显示进度条,让用户知道工具正在工作,而非卡死。

3. 统计结果的数据结构: 扫描和统计完成后,工具会在内存中构建一个数据结构,用于存储和传递给视图层。这个结构可能类似于:

interface StatsResult {
  summary: {
    totalFiles: number;
    totalLines: number;
    totalSize: number; // 总字节数
  };
  byLanguage: Array<{
    language: string;
    files: number;
    lines: number;
    size: number;
  }>;
  byFolder?: Array<{...}>; // 可选:按文件夹统计
  largestFiles?: Array<{...}>; // 可选:最大的十个文件
}

3.3 数据可视化与交互界面设计

数据统计出来,如何清晰美观地呈现给用户,决定了工具的用户体验上限。

1. 视图位置选择:

  • 侧边栏视图 (Sidebar View) :这是最自然的位置。可以创建一个新的活动栏图标(Activity Bar Icon),点击后在主侧边栏区域显示统计面板。这样不占用编辑区域,随时可以查看或隐藏。
  • 状态栏信息 (Status Bar) :可以在编辑器底部的状态栏显示最核心的摘要信息,如 📊 总计: 142 文件, 5,328 行 。点击状态栏可以快速触发详细面板的弹出或聚焦。
  • Webview 面板 (Webview Panel) :也可以创建一个新的编辑器标签页来显示丰富的图表。这种方式展示面积大,但会占用一个编辑组。

我倾向于 cursor-stats 采用 侧边栏视图 + 状态栏摘要 的组合方式。侧边栏承载完整功能,状态栏提供快速入口和概览。

2. 可视化图表库选型: 在 Webview 中渲染图表,需要引入前端图表库。选型要考虑:

  • 体积小 :扩展包体积需要控制。
  • 易用性 :API 简单,能快速实现饼图、条形图等基础图表。
  • 无 DOM 冲突 :需要能在 Webview 的隔离环境中良好运行。 Chart.js 是一个非常好的选择。它体积适中,功能全面,文档丰富,并且通过 chart.js/auto 可以方便地打包。如果追求极致轻量, Apache ECharts 的按需打包功能也很强大,但配置稍复杂。

3. 关键交互功能: 一个优秀的统计工具不能只是“一张死的报表”,而应该有交互:

  • 图表联动 :点击饼图中的“TypeScript”区块,下方的文件列表应自动过滤出所有 .ts .tsx 文件。
  • 排序功能 :文件列表可以按文件名、行数、大小进行升序/降序排列。
  • 路径导航 :点击列表中的某个文件,应能在编辑器中直接打开该文件。
  • 刷新按钮 :手动触发重新统计。
  • 导出数据 :允许将统计结果导出为 JSON 或 CSV 格式,方便进一步分析或生成报告。

4. 从零开始:实现一个简易版 cursor-stats

理解了核心原理后,我们可以动手实现一个具备基础功能的简易版本。这能帮助我们更深刻地体会其中的细节。

4.1 开发环境搭建与扩展骨架创建

首先,确保你安装了 Node.js 和 Cursor(或 VS Code)。

  1. 安装 Yeoman 和 VS Code 扩展生成器

    npm install -g yo generator-code
    
  2. 创建新扩展项目

    yo code
    

    在交互式命令行中:

    • 选择 New Extension (TypeScript)
    • 输入扩展名,例如 my-cursor-stats
    • 输入一个标识符
    • 输入描述
    • 是否初始化 Git 仓库?按需选择。
    • 打开扩展开发宿主?选择 None ,我们稍后手动打开。
  3. 打开项目并安装依赖 : 用 Cursor 打开生成的文件夹,在终端执行 npm install

  4. 项目结构初览

    • src/extension.ts :扩展的入口文件,激活和注册命令的地方。
    • package.json :扩展的清单文件,定义了命令、视图、配置等。
    • tsconfig.json :TypeScript 配置。
    • node_modules/ :依赖库。

4.2 核心统计逻辑的实现

我们在 src 目录下创建一个新的文件 statsCore.ts ,专门负责文件扫描和统计。

// src/statsCore.ts
import * as vscode from 'vscode';
import * as fs from 'fs/promises';
import * as path from 'path';

// 语言-后缀映射
const LANGUAGE_MAP: { [key: string]: string } = {
    '.js': 'JavaScript',
    '.ts': 'TypeScript',
    '.jsx': 'React (JS)',
    '.tsx': 'React (TS)',
    '.py': 'Python',
    '.java': 'Java',
    '.go': 'Go',
    '.css': 'CSS',
    '.html': 'HTML',
    '.json': 'JSON',
    '.md': 'Markdown',
    // 可根据需要扩展
};

export interface FileStat {
    path: string;
    name: string;
    language: string;
    lines: number;
    size: number;
}

export interface StatsSummary {
    totalFiles: number;
    totalLines: number;
    totalSize: number;
    byLanguage: { [language: string]: { files: number; lines: number; size: number } };
    fileStats: FileStat[];
}

export async function scanWorkspace(): Promise<StatsSummary> {
    // 1. 获取用户配置的忽略模式
    const config = vscode.workspace.getConfiguration('myCursorStats');
    const ignorePatterns = config.get<string[]>('ignorePatterns', ['**/.git/**', '**/node_modules/**']);
    const excludeGlob = `{${ignorePatterns.join(',')}}`;

    // 2. 查找所有文件
    const uris = await vscode.workspace.findFiles('**/*', excludeGlob);
    
    const fileStats: FileStat[] = [];
    let totalLines = 0;
    let totalSize = 0;
    const byLanguage: { [key: string]: { files: number; lines: number; size: number } } = {};

    // 3. 限制并发,防止文件过多导致问题
    const concurrencyLimit = 50;
    for (let i = 0; i < uris.length; i += concurrencyLimit) {
        const batch = uris.slice(i, i + concurrencyLimit);
        const promises = batch.map(async (uri) => {
            try {
                const filePath = uri.fsPath;
                const ext = path.extname(filePath).toLowerCase();
                const language = LANGUAGE_MAP[ext] || `Other (${ext || 'no ext'})`;
                
                const stats = await fs.stat(filePath);
                const size = stats.size;
                
                // 只处理文本文件,且大小不超过 1MB
                if (stats.isFile() && size < 1024 * 1024) {
                    const content = await fs.readFile(filePath, 'utf-8');
                    const lines = content.split('\n').length; // 物理行数
                    
                    const fileStat: FileStat = {
                        path: filePath,
                        name: path.basename(filePath),
                        language,
                        lines,
                        size
                    };
                    
                    // 更新按语言统计
                    if (!byLanguage[language]) {
                        byLanguage[language] = { files: 0, lines: 0, size: 0 };
                    }
                    byLanguage[language].files += 1;
                    byLanguage[language].lines += lines;
                    byLanguage[language].size += size;
                    
                    totalLines += lines;
                    totalSize += size;
                    
                    return fileStat;
                }
            } catch (error) {
                console.error(`Error processing file ${uri.fsPath}:`, error);
            }
            return null;
        });
        
        const results = await Promise.all(promises);
        results.filter(stat => stat !== null).forEach(stat => fileStats.push(stat!));
    }

    return {
        totalFiles: fileStats.length,
        totalLines,
        totalSize,
        byLanguage,
        fileStats
    };
}

这个 scanWorkspace 函数完成了核心的扫描、分类和统计工作。它考虑了并发控制、文件大小过滤和错误处理。

4.3 构建侧边栏视图与 Webview 面板

接下来,我们需要创建一个视图来展示统计结果。我们在 src 下创建 statsView.ts statsPanel.ts

首先,在 package.json 中声明我们的视图和命令:

{
  "contributes": {
    "views": {
      "explorer": [
        {
          "id": "myCursorStats.sidebarView",
          "name": "Code Stats"
        }
      ]
    },
    "commands": [
      {
        "command": "myCursorStats.refreshStats",
        "title": "Refresh Code Statistics"
      },
      {
        "command": "myCursorStats.openStatsPanel",
        "title": "Open Detailed Stats Panel"
      }
    ],
    "configuration": {
      "title": "My Cursor Stats",
      "properties": {
        "myCursorStats.ignorePatterns": {
          "type": "array",
          "default": ["**/.git/**", "**/node_modules/**", "**/dist/**", "**/*.min.js"],
          "description": "Glob patterns to ignore when scanning files."
        }
      }
    }
  }
}

然后,实现一个简单的 TreeDataProvider 用于侧边栏:

// src/statsView.ts
import * as vscode from 'vscode';
import { StatsSummary } from './statsCore';

export class StatsTreeDataProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
    private _onDidChangeTreeData: vscode.EventEmitter<vscode.TreeItem | undefined | null | void> = new vscode.EventEmitter();
    readonly onDidChangeTreeData: vscode.Event<vscode.TreeItem | undefined | null | void> = this._onDidChangeTreeData.event;

    private stats: StatsSummary | null = null;

    refresh(stats: StatsSummary): void {
        this.stats = stats;
        this._onDidChangeTreeData.fire();
    }

    getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
        return element;
    }

    getChildren(element?: vscode.TreeItem): Thenable<vscode.TreeItem[]> {
        if (!this.stats) {
            return Promise.resolve([new vscode.TreeItem('No data. Run a scan first.')]);
        }

        if (!element) {
            // 根节点:显示摘要
            const summaryItems = [
                new vscode.TreeItem(`Files: ${this.stats.totalFiles}`, vscode.TreeItemCollapsibleState.None),
                new vscode.TreeItem(`Lines: ${this.stats.totalLines.toLocaleString()}`, vscode.TreeItemCollapsibleState.None),
                new vscode.TreeItem(`Size: ${(this.stats.totalSize / 1024).toFixed(2)} KB`, vscode.TreeItemCollapsibleState.None),
                new vscode.TreeItem('By Language', vscode.TreeItemCollapsibleState.Collapsed)
            ];
            return Promise.resolve(summaryItems);
        } else if (element.label === 'By Language') {
            // 语言分类子节点
            const langItems = Object.entries(this.stats.byLanguage).map(([lang, data]) => {
                const item = new vscode.TreeItem(`${lang}: ${data.files} files, ${data.lines} lines`);
                item.tooltip = `Click to filter files`;
                item.command = {
                    command: 'myCursorStats.filterByLanguage',
                    title: 'Filter',
                    arguments: [lang]
                };
                return item;
            });
            return Promise.resolve(langItems);
        }
        return Promise.resolve([]);
    }
}

这个树形视图提供了基础的摘要信息。要实现更丰富的图表,我们需要一个 Webview 面板。由于篇幅限制,这里不展开完整的 Webview 实现代码,但其核心步骤是:

  1. extension.ts 中注册一个命令(如 openStatsPanel )。
  2. 当命令触发时,创建并显示一个 WebviewPanel
  3. 在 Webview 的 HTML 中引入 Chart.js 库(通过 CDN 或本地资源)。
  4. scanWorkspace 得到的 StatsSummary 数据序列化后传递给 Webview。
  5. 在 Webview 的 JavaScript 中,使用 Chart.js 将 byLanguage 数据渲染成饼图或条形图,并将 fileStats 渲染成可排序、可过滤的表格。

4.4 集成与激活:让扩展运行起来

最后,我们需要在 src/extension.ts 中将这些部分串联起来。

// src/extension.ts
import * as vscode from 'vscode';
import { StatsTreeDataProvider } from './statsView';
import { scanWorkspace } from './statsCore';
import { StatsPanel } from './statsPanel'; // 假设我们创建了这个类

export function activate(context: vscode.ExtensionContext) {
    console.log('Extension "my-cursor-stats" is now active!');

    // 初始化侧边栏视图
    const statsTreeDataProvider = new StatsTreeDataProvider();
    vscode.window.registerTreeDataProvider('myCursorStats.sidebarView', statsTreeDataProvider);

    // 注册刷新命令
    const refreshCommand = vscode.commands.registerCommand('myCursorStats.refreshStats', async () => {
        vscode.window.withProgress({
            location: vscode.ProgressLocation.Notification,
            title: 'Scanning workspace for code statistics...',
            cancellable: false
        }, async (progress) => {
            const stats = await scanWorkspace();
            statsTreeDataProvider.refresh(stats);
            vscode.window.showInformationMessage(`Scan complete! Found ${stats.totalFiles} files with ${stats.totalLines} lines.`);
        });
    });

    // 注册打开详情面板命令
    const openPanelCommand = vscode.commands.registerCommand('myCursorStats.openStatsPanel', () => {
        StatsPanel.createOrShow(context.extensionUri);
    });

    // 将命令添加到订阅列表,以便在停用时销毁
    context.subscriptions.push(refreshCommand, openPanelCommand);

    // 可选:激活后自动运行一次扫描
    // refreshCommand.callback();
}

export function deactivate() {}

至此,一个具备基础文件扫描、分类统计、侧边栏摘要显示功能的简易版 cursor-stats 就完成了。你可以按 F5 启动一个扩展开发宿主窗口,在新窗口中打开一个项目文件夹,然后运行 Refresh Code Statistics 命令来测试效果。

5. 高级特性探讨与优化方向

一个基础的统计工具实现起来并不复杂,但要让它变得好用、强大,就需要考虑更多细节和高级功能。这些也是像 Dwtexe/cursor-stats 这样的成熟项目可能已经实现或正在规划的方向。

5.1 性能优化:应对巨型代码仓库的挑战

当项目规模达到像 Linux Kernel、Chromium 这种级别(数十万个文件)时,简单的遍历和读取就会变得非常缓慢。优化策略包括:

  1. 增量统计与智能缓存

    • 不仅缓存最终结果,更缓存每个文件的哈希值(如 MD5)和行数。
    • 监听工作区的文件更改事件( onDidCreateFiles , onDidDeleteFiles , onDidSaveTextDocument )。
    • 当文件被修改并保存时,只需重新计算该文件的统计信息,并更新总缓存。删除或新增文件同理。
    • 这样,第二次及之后的统计几乎是瞬间完成的。
  2. 异步与分片处理

    • 将文件列表分成多个小块(例如每1000个文件一块),使用 setImmediate requestIdleCallback (在 Webview 中)进行分片处理,避免阻塞事件循环。
    • 在扫描过程中,如果用户切换了工作区或关闭了视图,应能优雅地取消正在进行的扫描任务。
  3. 提供统计范围选择

    • 允许用户只统计当前打开的文件夹,而不是整个工作区。
    • 提供“快速扫描”(仅统计文件数量和类型)和“深度扫描”(统计行数和大小)两种模式。

5.2 数据持久化与历史对比

单纯的当前快照价值有限,如果能看到历史变化趋势,工具的价值会倍增。

  1. 本地存储统计快照

    • 每次统计完成后,将结果( StatsSummary )连同时间戳和当前 Git 提交哈希(如果可用)一起保存到扩展的全局存储( context.globalState )或工作区存储中。
    • 存储时可以考虑使用压缩算法(如 pako 压缩 JSON)来节省空间。
  2. 历史趋势可视化

    • 在 Webview 面板中增加一个“历史”选项卡。
    • 使用折线图展示总文件数、总代码行数随时间的变化。
    • 可以对比不同日期的数据,直观看到项目的增长情况。
  3. 与版本控制系统集成

    • 这是一个更高级的功能。通过解析 Git 历史,可以统计出每个贡献者的代码行数(需谨慎对待,避免引发“行数竞赛”)。
    • 可以展示本次提交(或未提交的更改)增加了/删除了多少行代码。

5.3 自定义规则与扩展性设计

不同的项目、不同的团队有不同的统计需求。一个好的工具应该允许一定程度的自定义。

  1. 自定义语言映射

    • 允许用户在设置中覆盖或新增文件后缀到语言名称的映射。例如,将 .vue 文件单独归类,或者将 .h .cpp 都归为“C++”。
  2. 自定义统计规则

    • 允许用户为正则表达式匹配的文件定义特定的统计方式。例如,对于 *.test.js 文件,用户可能希望将它们归类到“测试代码”而不是“JavaScript”。
    • 甚至可以允许用户写一小段 JavaScript 函数,对每行代码进行解析,以实现更复杂的逻辑行数统计(如排除特定格式的注释)。
  3. 插件化架构(远期)

    • 定义一套统计器( Counter )接口。内置一个“物理行数统计器”。
    • 允许其他扩展注册新的统计器,例如“逻辑行数统计器(针对Java)”、“有效代码复杂度统计器”等。
    • 这样,工具的核心就变成了一个可插拔的统计框架,潜力巨大。

6. 常见问题与实战排查技巧

在实际开发和使用这类工具的过程中,你肯定会遇到各种各样的问题。下面是我总结的一些典型场景和解决思路。

6.1 扫描速度慢,甚至导致编辑器卡顿

问题现象 :点击刷新后,编辑器失去响应,状态栏一直转圈,很久才有结果。

排查与解决

  1. 检查忽略规则 :首先确认 ignorePatterns 是否正确配置,是否排除了 node_modules , .git , dist , build 等通常包含大量非源码文件的目录。一个错误的规则可能导致工具扫描数万个无用文件。
  2. 检查文件大小限制 :确认代码中是否有对超大文件(如图片、视频、数据库 dump 文件)的跳过机制。读取一个几百MB的日志文件会消耗大量时间和内存。
  3. 查看输出通道 :VS Code 扩展可以通过 vscode.window.createOutputChannel 创建一个专属的输出通道,用于打印调试日志。在扫描过程中,记录下正在处理的文件路径和耗时,有助于定位是卡在哪个特定文件或目录上。
  4. 实施并发控制 :如我们之前代码所示,一定要用 p-limit 或手动分片的方式限制并发文件读取数(如 20-50)。一次性发起上千个 fs.readFile 的 Promise 可能会导致性能问题。
  5. 考虑使用 Worker :对于计算密集型任务,可以尝试使用 Web Worker(在 Webview 中)或 Node.js 的 Worker Threads(在扩展主机中)将统计任务放到单独的线程中,避免阻塞主线程。

6.2 统计结果不准确或不符合预期

问题现象 :统计的行数和自己用 wc -l 命令或其它工具统计的结果对不上;某些文件没有被统计进去。

排查与解决

  1. 行尾符差异 :Windows 使用 \r\n ,而 Unix/Linux 使用 \n 。使用 content.split(/\r?\n/).length 比简单的 split('\n') 更健壮,能正确处理两种行尾符。
  2. 二进制文件误判 :仅通过文件后缀或大小来判断是否为文本文件并不完全可靠。可以尝试读取文件的前几个字节,检查是否包含大量的空字符( \0 ),这是二进制文件的常见特征。更简单的方法是,在 try...catch 中用 utf-8 编码读取,如果抛出异常或解码出大量乱码,则可能是二进制文件。
  3. 软链接/符号链接 vscode.workspace.findFiles 默认会跟随符号链接吗?文档需要查证。如果不希望统计符号链接指向的目标文件(可能导致重复统计),可能需要额外的逻辑来处理。
  4. 编码问题 :确保使用 utf-8 编码读取文件。对于其它编码(如 GBK)的文件,统计可能会失败或得到错误的行数。一个生产级的工具可能需要借助 iconv-lite 这样的库来尝试多种编码。
  5. 验证忽略模式 :手动检查一下,你认为应该被统计的文件,其路径是否意外匹配了你的 ignorePatterns 中的某条规则?Glob 模式有时会比较 tricky。

6.3 扩展激活失败或视图不显示

问题现象 :安装扩展后,侧边栏没有出现对应的图标,或者命令无法执行。

排查与解决

  1. 检查 package.json :这是最常见的原因。确保 activationEvents 配置正确。对于视图类扩展,通常需要 onView:${viewId} (例如 onView:myCursorStats.sidebarView )或更通用的 onStartupFinished 。确保 views commands id 与代码中注册的完全一致。
  2. 检查开发者工具 :在 Cursor/VS Code 中,通过 帮助 -> 切换开发者工具 打开控制台。任何扩展加载或运行时的 JavaScript 错误都会在这里显示。这是调试扩展问题的第一现场。
  3. 重新加载窗口 :在修改了 package.json extension.ts 的激活逻辑后,必须使用 Ctrl/Cmd + Shift + P 运行 Developer: Reload Window 命令,更改才会生效。
  4. 检查依赖 :如果你的扩展依赖了第三方 Node 模块(如 chart.js ),确保它已经正确安装在 node_modules 中,并且在 package.json dependencies 里声明。对于 Webview 中使用的库,如果通过 CDN 引入,要确保网络可达;如果打包为本地资源,要确保路径正确。

6.4 Webview 内容无法加载或图表不显示

问题现象 :统计面板打开了,但是一片空白,或者控制台有资源加载错误。

排查与解决

  1. 检查 Content Security Policy (CSP) :VS Code 的 Webview 有严格的 CSP 限制。在创建 WebviewPanel 时,必须通过 getHtmlForWebview 方法生成 HTML,并在 <meta> 标签中正确配置 CSP。如果需要在 Webview 中加载来自 https://cdn.jsdelivr.net 的 Chart.js,就必须在 CSP 中允许该域名。常见的错误是忘记了 script-src style-src 指令。
  2. 检查资源路径 :如果 Chart.js 是作为本地文件打包进扩展的,那么需要通过 webview.asWebviewUri 方法将本地资源路径转换为 Webview 可以加载的特殊 URI。直接使用 file:// 路径是行不通的。
  3. 验证数据传递 :确保从扩展端到 Webview 的数据传递成功了。可以在 Webview 的 JavaScript 开头加一句 console.log('Data received:', data) 来检查。数据需要通过 postMessage 传递,并在 Webview 中通过 window.addEventListener('message', ...) 来接收。
  4. 查看 Webview 开发者工具 :在 Webview 面板内右键单击,选择“检查元素”,可以打开专门针对这个 Webview 的开发者工具。在这里看到的错误信息,是解决 Webview 内部问题的关键。

开发这样一个工具,从简单的原型到稳定可用的产品,中间会遇到不少“坑”。但每解决一个问题,你对 Cursor/VS Code 扩展系统的理解就会加深一层,最终得到的不仅是一个有用的工具,更是一份宝贵的经验。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐