feat(viper): 新增「统计项目 LOC」命令与报表 Webview
- 右键 .vpj → Count Project LOC(viper.countLines):递归扫描项目的 source_dir, 统计文件数 / 总行 / 代码行 / 注释行 / 空行 / 注释率 / 平均行长 / 字符数 - 先打开 Viper LOC Webview 面板再扫描:确定性进度条(先收集文件,再按 N/M 处理 并显示当前文件),完成后渲染 KPI 卡片、按语言的堆叠条形图(代码/注释/空行, 以最大语言为基准缩放)、语言筛选(勾选后重算 KPI/图表/表格)与明细表(含合计行) - 同一份报表以纯文本写入 Viper LOC 输出通道,便于复制 - 计入本扩展注册的语言扩展名 .vp / .vpi / .vpnci / .vpj,并按需求支持 .def (; 与 // 行注释、/* */ 块注释,跨行块注释按注释计) - 跳过项目 build_dir、隐藏目录、node_modules、.git、__pycache__,不跟随符号链接
This commit is contained in:
1 parent
aa1c84a326
commit
b98102b78f
4 files changed
+1193
-1
No files matched your search
@@ -162,6 +162,11 @@
|
||||
"command": "viper.openVpjAsJson",
|
||||
"title": "Open .vpj as JSON",
|
||||
"category": "Viper"
|
||||
},
|
||||
{
|
||||
"command": "viper.countLines",
|
||||
"title": "Count Project LOC",
|
||||
"category": "Viper"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -184,6 +189,10 @@
|
||||
{
|
||||
"command": "viper.openVpjAsJson",
|
||||
"when": "resourceExtname == .vpj"
|
||||
},
|
||||
{
|
||||
"command": "viper.countLines",
|
||||
"when": "resourceExtname == .vpj"
|
||||
}
|
||||
],
|
||||
"editor/title/context": [
|
||||
@@ -198,6 +207,11 @@
|
||||
"command": "viper.openVpjAsJson",
|
||||
"when": "resourceExtname == .vpj",
|
||||
"group": "1_open"
|
||||
},
|
||||
{
|
||||
"command": "viper.countLines",
|
||||
"when": "resourceExtname == .vpj",
|
||||
"group": "1_open@2"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { commands, ExtensionContext, ProgressLocation, window, workspace } from 'vscode';
|
||||
import { commands, ExtensionContext, OutputChannel, ProgressLocation, Uri, window, workspace } from 'vscode';
|
||||
import { EnvironmentManager } from './environmentManager';
|
||||
import { EnvironmentTool } from './environmentTool';
|
||||
import { CountProjectLoc, FormatLocReport, LocReport } from './lineCounter';
|
||||
import { LocReportView } from './locReportView';
|
||||
import { ProjectManager } from './projectManager';
|
||||
import { ToolchainManager } from './toolchainManager';
|
||||
import { ViperCheckResult, ViperEnvironment } from './types';
|
||||
@@ -25,6 +27,10 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
|
||||
const environmentTool = new EnvironmentTool(context.globalStorageUri.fsPath, vetChannel);
|
||||
const environments = new EnvironmentManager(context.globalState);
|
||||
|
||||
// LOC 统计输出通道(右键 .vpj → 统计该工程源目录下所有已知语言的行数)。
|
||||
const locChannel = window.createOutputChannel('Viper LOC');
|
||||
context.subscriptions.push(locChannel);
|
||||
|
||||
// 初始扫描项目文件。
|
||||
await projects.DiscoverProjects();
|
||||
|
||||
@@ -148,6 +154,9 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
|
||||
}
|
||||
// 用默认文本编辑器打开,绕过自定义 webview 编辑器。
|
||||
await window.showTextDocument(target, { preview: false });
|
||||
}),
|
||||
RegisterCommandSafe(context, 'viper.countLines', async (uri?: unknown) => {
|
||||
await CountProjectLocCommand(context, projects, locChannel, IsUri(uri) ? uri : undefined);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -382,3 +391,67 @@ function ShowProjectInfoCommand(projects: ProjectManager): void {
|
||||
channel.appendLine(lines.join('\n'));
|
||||
channel.show();
|
||||
}
|
||||
|
||||
/** 判断命令参数是否为 Uri(右键菜单传入的第一个参数)。 */
|
||||
function IsUri(value: unknown): value is Uri {
|
||||
return !!value && typeof value === 'object' && typeof (value as Uri).fsPath === 'string';
|
||||
}
|
||||
|
||||
/** 取本命令要统计的工程文件:右键传入的 .vpj → 当前活动编辑器(是 .vpj 时)。 */
|
||||
function ResolveProjectFile(uri?: Uri): Uri | undefined {
|
||||
const isProjectFile = (candidate: Uri | undefined): boolean =>
|
||||
!!candidate && path.extname(candidate.fsPath).toLowerCase() === '.vpj';
|
||||
|
||||
if (isProjectFile(uri)) {
|
||||
return uri;
|
||||
}
|
||||
const activeUri = window.activeTextEditor?.document.uri;
|
||||
return isProjectFile(activeUri) ? activeUri : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「统计项目 LOC」:右键 .vpj → 统计该工程源目录(source_dir,通常为 App)下
|
||||
* 所有已知语言(.vp/.vpi/.vpnci/.vpj/.def)的文件数与行数,
|
||||
* 结果在 Webview 面板里以进度条 + 图表 + 明细表展示(另存一份纯文本到输出通道)。
|
||||
* 未传文件参数(命令面板)时退回当前工作区已加载的工程。
|
||||
*/
|
||||
async function CountProjectLocCommand(
|
||||
context: ExtensionContext,
|
||||
projects: ProjectManager,
|
||||
channel: OutputChannel,
|
||||
uri?: Uri
|
||||
): Promise<void> {
|
||||
const projectFile = ResolveProjectFile(uri);
|
||||
// 右键的 .vpj 可能不是 DiscoverProjects 留下(每个工作区只保留最后一个)的那个,
|
||||
// 故按点击的文件重新解析,保证统计的就是被点中的工程。
|
||||
const project = projectFile ? projects.LoadProject(projectFile) : projects.GetProject();
|
||||
if (!project) {
|
||||
void window.showWarningMessage('未找到 Viper 工程:请对 .vpj 文件使用本命令。');
|
||||
return;
|
||||
}
|
||||
|
||||
const view = LocReportView.Show(context);
|
||||
view.StartRun(project.Name, project.ProjectFile, project.SourceDir);
|
||||
|
||||
let report: LocReport;
|
||||
try {
|
||||
report = await CountProjectLoc(project.SourceDir, [project.BuildDir], (progress) => {
|
||||
view.ReportProgress(progress);
|
||||
});
|
||||
} catch (error) {
|
||||
view.ReportError(`统计失败:${error instanceof Error ? error.message : String(error)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 纯文本一份留档(便于复制),主力展示在 Webview。
|
||||
channel.clear();
|
||||
channel.appendLine(FormatLocReport(report, project.Name, project.ProjectFile));
|
||||
|
||||
view.ReportResult({ ...report, ProjectName: project.Name, ProjectFile: project.ProjectFile });
|
||||
|
||||
if (report.Total.FileCount > 0) {
|
||||
void window.showInformationMessage(
|
||||
`LOC:${report.Total.CodeLines} 行代码 / ${report.Total.TotalLines} 行(${report.Total.FileCount} 个文件,注释率 ${(report.Total.CommentRate * 100).toFixed(1)}%)`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* lineCounter.ts
|
||||
* 项目 LOC(代码行)统计:递归扫描工程源目录,按「已知语言」汇总文件数与行数。
|
||||
*
|
||||
* 只依赖 Node 的 fs/path,不引用 vscode,便于单独复用与测试;
|
||||
* 展示层(locReportView.ts)负责进度条、图表与报表渲染。
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/** 一种已知语言:扩展名集合 + 注释语法。 */
|
||||
export interface KnownLanguage {
|
||||
/** 语言显示名(报表中的一行)。 */
|
||||
Name: string;
|
||||
/** 归属该语言的扩展名(小写,含点)。 */
|
||||
Extensions: string[];
|
||||
/** 行注释前缀;无行注释时为空数组。 */
|
||||
LineCommentPrefixes: string[];
|
||||
/** 块注释定界符;无块注释时留空。 */
|
||||
BlockComments?: { Start: string; End: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 已知语言表(统计范围)。
|
||||
* Viper 系列与 vscode-vipeye 的 languages 贡献点一致(.vp/.vpi/.vpnci,另有 .vpj 工程文件),
|
||||
* 并按用户要求支持 .def(定义文件):既可能是 MODULE-DEFINITION 的 ';' 行注释,
|
||||
* 也可能是 C 风格的 '//' 行注释与块注释,故三者都识别。
|
||||
*/
|
||||
export const KnownLanguages: KnownLanguage[] = [
|
||||
{ Name: 'Viper', Extensions: ['.vp'], LineCommentPrefixes: ['#'] },
|
||||
{ Name: 'Viper Stub', Extensions: ['.vpi'], LineCommentPrefixes: ['#'] },
|
||||
{ Name: 'Viper Annotation', Extensions: ['.vpnci'], LineCommentPrefixes: ['#'] },
|
||||
{ Name: 'Viper Project', Extensions: ['.vpj'], LineCommentPrefixes: [] },
|
||||
{
|
||||
Name: 'Definition',
|
||||
Extensions: ['.def'],
|
||||
LineCommentPrefixes: [';', '//'],
|
||||
BlockComments: [{ Start: '/*', End: '*/' }],
|
||||
},
|
||||
];
|
||||
|
||||
/** 扩展名 → 语言(由 KnownLanguages 展开)。 */
|
||||
const ExtensionToLanguage = new Map<string, KnownLanguage>();
|
||||
for (const language of KnownLanguages) {
|
||||
for (const extension of language.Extensions) {
|
||||
ExtensionToLanguage.set(extension, language);
|
||||
}
|
||||
}
|
||||
|
||||
/** 固定跳过的目录名:依赖、版本库元数据、字节码缓存。 */
|
||||
const SkippedDirectoryNames = new Set(['node_modules', '.git', '__pycache__']);
|
||||
|
||||
/** 单语言 / 汇总的行数统计。 */
|
||||
export interface LineCount {
|
||||
FileCount: number;
|
||||
TotalLines: number;
|
||||
CodeLines: number;
|
||||
CommentLines: number;
|
||||
BlankLines: number;
|
||||
/** 字符数(不含换行符),用于算平均行字符数。 */
|
||||
Chars: number;
|
||||
}
|
||||
|
||||
/** 派生指标。 */
|
||||
export interface LocMetrics {
|
||||
/** 注释率 = 注释行 /(代码行 + 注释行);无非空行时为 0。 */
|
||||
CommentRate: number;
|
||||
/** 平均行字符数 = 字符数 / 总行数。 */
|
||||
AverageLineLength: number;
|
||||
}
|
||||
|
||||
/** 报表的一行(语言明细 / 合计)。 */
|
||||
export interface LocReportRow extends LineCount, LocMetrics {
|
||||
Language: string;
|
||||
}
|
||||
|
||||
/** 一次 LOC 统计的结果。 */
|
||||
export interface LocReport {
|
||||
/** 被扫描的源目录(绝对路径)。 */
|
||||
SourceDir: string;
|
||||
/** 扫描时跳过的子目录(相对源目录的路径)。 */
|
||||
SkippedDirectories: string[];
|
||||
/** 按语言的汇总(代码行降序,只含确实存在文件的语言)。 */
|
||||
Languages: LocReportRow[];
|
||||
/** 全部已知语言的合计。 */
|
||||
Total: LocReportRow;
|
||||
}
|
||||
|
||||
/** 扫描进度(用于进度条)。 */
|
||||
export interface LocScanProgress {
|
||||
/** 已统计完成的文件数。 */
|
||||
ProcessedFiles: number;
|
||||
/** 待统计的已知语言文件总数。 */
|
||||
TotalFiles: number;
|
||||
/** 当前文件(相对源目录)。 */
|
||||
CurrentFile: string;
|
||||
}
|
||||
|
||||
/** 行分类:代码 / 注释 / 空行。 */
|
||||
type LineKind = 'code' | 'comment' | 'blank';
|
||||
|
||||
/** 逐行扫描的跨行状态(块注释未闭合时的结束定界符)。 */
|
||||
interface ScanState {
|
||||
InBlockCommentEnd: string | undefined;
|
||||
}
|
||||
|
||||
/** 待统计的文件。 */
|
||||
interface ScannedFile {
|
||||
FilePath: string;
|
||||
Language: KnownLanguage;
|
||||
}
|
||||
|
||||
/** 创建全 0 的行数计数。 */
|
||||
export function CreateEmptyLineCount(): LineCount {
|
||||
return { FileCount: 0, TotalLines: 0, CodeLines: 0, CommentLines: 0, BlankLines: 0, Chars: 0 };
|
||||
}
|
||||
|
||||
/** 把 source 累加进 target(原地)。 */
|
||||
export function AccumulateLineCount(target: LineCount, source: LineCount): void {
|
||||
target.FileCount += source.FileCount;
|
||||
target.TotalLines += source.TotalLines;
|
||||
target.CodeLines += source.CodeLines;
|
||||
target.CommentLines += source.CommentLines;
|
||||
target.BlankLines += source.BlankLines;
|
||||
target.Chars += source.Chars;
|
||||
}
|
||||
|
||||
/** 注释率 = 注释行 /(代码行 + 注释行)。 */
|
||||
export function ComputeCommentRate(count: LineCount): number {
|
||||
const nonBlankLines = count.CodeLines + count.CommentLines;
|
||||
return nonBlankLines > 0 ? count.CommentLines / nonBlankLines : 0;
|
||||
}
|
||||
|
||||
/** 平均行字符数 = 字符数 / 总行数。 */
|
||||
export function ComputeAverageLineLength(count: LineCount): number {
|
||||
return count.TotalLines > 0 ? count.Chars / count.TotalLines : 0;
|
||||
}
|
||||
|
||||
/** 取文件所属的已知语言;未登记(或没有扩展名)返回 undefined。 */
|
||||
export function GetKnownLanguage(filePath: string): KnownLanguage | undefined {
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
if (!extension) {
|
||||
return undefined;
|
||||
}
|
||||
return ExtensionToLanguage.get(extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计一段文本的行数(纯函数,按语言的注释语法分类)。
|
||||
* 行尾的最后一个换行不算作额外空行;跨行的块注释正确归为注释。
|
||||
*/
|
||||
export function CountTextLines(content: string, language: KnownLanguage): LineCount {
|
||||
const count = CreateEmptyLineCount();
|
||||
count.FileCount = 1;
|
||||
|
||||
const lines = content.split(/\r\n|\r|\n/);
|
||||
// "a\n" 切出 ['a', '']:结尾换行不产生额外空行。
|
||||
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
const state: ScanState = { InBlockCommentEnd: undefined };
|
||||
for (const line of lines) {
|
||||
count.TotalLines += 1;
|
||||
count.Chars += line.length;
|
||||
switch (ClassifyLine(line, language, state)) {
|
||||
case 'code':
|
||||
count.CodeLines += 1;
|
||||
break;
|
||||
case 'comment':
|
||||
count.CommentLines += 1;
|
||||
break;
|
||||
default:
|
||||
count.BlankLines += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/** 读取并统计单个文件;读取失败按全 0 计(不中断整次统计)。 */
|
||||
export async function CountFileLines(filePath: string, language: KnownLanguage): Promise<LineCount> {
|
||||
try {
|
||||
const content = await fs.promises.readFile(filePath, 'utf8');
|
||||
return CountTextLines(content, language);
|
||||
} catch {
|
||||
return CreateEmptyLineCount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计工程源目录下所有已知语言的行数。
|
||||
* extraSkipDirectories 传工程构建目录(源目录内的构建产物不参与统计)。
|
||||
* onProgress 每读完一个文件回调一次(先回调一次 ProcessedFiles = 0 的总数播报)。
|
||||
*/
|
||||
export async function CountProjectLoc(
|
||||
sourceDir: string,
|
||||
extraSkipDirectories: string[] = [],
|
||||
onProgress?: (progress: LocScanProgress) => void
|
||||
): Promise<LocReport> {
|
||||
const skippedDirectories: string[] = [];
|
||||
const files: ScannedFile[] = [];
|
||||
const extraSkipPaths = new Set(extraSkipDirectories.map((directory) => path.resolve(directory)));
|
||||
|
||||
// 阶段一:先收集文件清单,得到总数(进度条的分母)。
|
||||
await CollectKnownFiles(sourceDir, sourceDir, extraSkipPaths, skippedDirectories, files);
|
||||
|
||||
const totalFiles = files.length;
|
||||
onProgress?.({ ProcessedFiles: 0, TotalFiles: totalFiles, CurrentFile: '' });
|
||||
|
||||
// 阶段二:逐个统计并汇报进度。
|
||||
const counts = new Map<string, LineCount>();
|
||||
for (const [index, file] of files.entries()) {
|
||||
const current = counts.get(file.Language.Name) ?? CreateEmptyLineCount();
|
||||
AccumulateLineCount(current, await CountFileLines(file.FilePath, file.Language));
|
||||
counts.set(file.Language.Name, current);
|
||||
|
||||
onProgress?.({
|
||||
ProcessedFiles: index + 1,
|
||||
TotalFiles: totalFiles,
|
||||
CurrentFile: path.relative(sourceDir, file.FilePath),
|
||||
});
|
||||
}
|
||||
|
||||
// 只保留确实存在文件的语言,按代码行降序。
|
||||
const languages: LocReportRow[] = [];
|
||||
for (const language of KnownLanguages) {
|
||||
const count = counts.get(language.Name);
|
||||
if (count && count.FileCount > 0) {
|
||||
languages.push(CreateReportRow(language.Name, count));
|
||||
}
|
||||
}
|
||||
languages.sort((left, right) => right.CodeLines - left.CodeLines);
|
||||
|
||||
const total = CreateEmptyLineCount();
|
||||
for (const entry of languages) {
|
||||
AccumulateLineCount(total, entry);
|
||||
}
|
||||
|
||||
return {
|
||||
SourceDir: sourceDir,
|
||||
SkippedDirectories: skippedDirectories,
|
||||
Languages: languages,
|
||||
Total: CreateReportRow('Total', total),
|
||||
};
|
||||
}
|
||||
|
||||
/** 生成可读报表(等宽对齐,供输出通道展示)。 */
|
||||
export function FormatLocReport(report: LocReport, projectName: string, projectFile: string): string {
|
||||
const columnWidth = 12;
|
||||
const nameWidth = Math.max(
|
||||
18,
|
||||
'Language'.length,
|
||||
'Total'.length,
|
||||
...report.Languages.map((entry) => entry.Language.length)
|
||||
);
|
||||
const formatRow = (name: string, count: LineCount & LocMetrics): string =>
|
||||
name.padEnd(nameWidth) +
|
||||
String(count.FileCount).padStart(columnWidth) +
|
||||
String(count.TotalLines).padStart(columnWidth) +
|
||||
String(count.CodeLines).padStart(columnWidth) +
|
||||
String(count.CommentLines).padStart(columnWidth) +
|
||||
String(count.BlankLines).padStart(columnWidth) +
|
||||
`${(count.CommentRate * 100).toFixed(1)}%`.padStart(columnWidth) +
|
||||
count.AverageLineLength.toFixed(1).padStart(columnWidth);
|
||||
|
||||
const header =
|
||||
'Language'.padEnd(nameWidth) +
|
||||
'Files'.padStart(columnWidth) +
|
||||
'Total'.padStart(columnWidth) +
|
||||
'Code'.padStart(columnWidth) +
|
||||
'Comment'.padStart(columnWidth) +
|
||||
'Blank'.padStart(columnWidth) +
|
||||
'Comment%'.padStart(columnWidth) +
|
||||
'AvgLen'.padStart(columnWidth);
|
||||
|
||||
const lines: string[] = [
|
||||
'Viper LOC 统计',
|
||||
`项目: ${projectName}`,
|
||||
`工程文件: ${projectFile}`,
|
||||
`源目录: ${report.SourceDir}`,
|
||||
`跳过目录: ${report.SkippedDirectories.length > 0 ? report.SkippedDirectories.join(', ') : '(none)'}`,
|
||||
'',
|
||||
header,
|
||||
'-'.repeat(header.length),
|
||||
...report.Languages.map((entry) => formatRow(entry.Language, entry)),
|
||||
'-'.repeat(header.length),
|
||||
formatRow('Total', report.Total),
|
||||
];
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** 由行数计数构造带派生指标的报表行。 */
|
||||
function CreateReportRow(language: string, count: LineCount): LocReportRow {
|
||||
return {
|
||||
Language: language,
|
||||
...count,
|
||||
CommentRate: ComputeCommentRate(count),
|
||||
AverageLineLength: ComputeAverageLineLength(count),
|
||||
};
|
||||
}
|
||||
|
||||
/** 递归收集源目录下所有已知语言的文件(跳过目录与符号链接)。 */
|
||||
async function CollectKnownFiles(
|
||||
directory: string,
|
||||
sourceDir: string,
|
||||
extraSkipPaths: Set<string>,
|
||||
skippedDirectories: string[],
|
||||
files: ScannedFile[]
|
||||
): Promise<void> {
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
||||
} catch {
|
||||
// 无权限 / 被占用:跳过该目录,不中断整次统计。
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(directory, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (ShouldSkipDirectory(entry.name, entryPath, extraSkipPaths)) {
|
||||
skippedDirectories.push(path.relative(sourceDir, entryPath) || entry.name);
|
||||
continue;
|
||||
}
|
||||
await CollectKnownFiles(entryPath, sourceDir, extraSkipPaths, skippedDirectories, files);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 符号链接一律跳过:避免环与重复计数。
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const language = GetKnownLanguage(entryPath);
|
||||
if (language) {
|
||||
files.push({ FilePath: entryPath, Language: language });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 目录是否不参与统计:依赖/元数据目录、隐藏目录(含 .tpv_build 这类构建产物)、工程构建目录。 */
|
||||
function ShouldSkipDirectory(name: string, entryPath: string, extraSkipPaths: Set<string>): boolean {
|
||||
return SkippedDirectoryNames.has(name) || name.startsWith('.') || extraSkipPaths.has(entryPath);
|
||||
}
|
||||
|
||||
/** 判断单行的类别;块注释状态在 state 上跨行传递。 */
|
||||
function ClassifyLine(text: string, language: KnownLanguage, state: ScanState): LineKind {
|
||||
let rest = text;
|
||||
let sawComment = false;
|
||||
|
||||
for (;;) {
|
||||
if (state.InBlockCommentEnd !== undefined) {
|
||||
const endIndex = rest.indexOf(state.InBlockCommentEnd);
|
||||
if (endIndex < 0) {
|
||||
// 整行仍在块注释内。
|
||||
return 'comment';
|
||||
}
|
||||
rest = rest.slice(endIndex + state.InBlockCommentEnd.length);
|
||||
state.InBlockCommentEnd = undefined;
|
||||
sawComment = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const trimmed = rest.trimStart();
|
||||
if (trimmed.length === 0) {
|
||||
return sawComment ? 'comment' : 'blank';
|
||||
}
|
||||
|
||||
if (language.LineCommentPrefixes.some((prefix) => trimmed.startsWith(prefix))) {
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
const blockComment = (language.BlockComments ?? []).find((block) => trimmed.startsWith(block.Start));
|
||||
if (blockComment) {
|
||||
state.InBlockCommentEnd = blockComment.End;
|
||||
rest = trimmed.slice(blockComment.Start.length);
|
||||
sawComment = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
return 'code';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
/*
|
||||
* locReportView.ts
|
||||
* 「统计项目 LOC」报告面板:统计进度条 + KPI + 语言筛选 + 行数构成图 + 明细表。
|
||||
*
|
||||
* 数据由 lineCounter.ts 计算,本文件只负责展示:
|
||||
* - 面板单例复用(再次统计只刷新同一面板);
|
||||
* - 骨架 HTML 内联脚本(packages/viper 不在打包器的资源复制清单里,
|
||||
* 外部脚本要改 rspack 配置才能落地,故用 nonce 内联脚本);
|
||||
* - 数据经 postMessage 下发,Webview 脚本就绪(ready)前的数据先缓存。
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from 'vscode';
|
||||
|
||||
import { LocReport, LocScanProgress } from './lineCounter';
|
||||
|
||||
/** 报表头部信息(工程名 / 工程文件 / 源目录)。 */
|
||||
export interface LocReportHeader {
|
||||
ProjectName: string;
|
||||
ProjectFile: string;
|
||||
SourceDir: string;
|
||||
}
|
||||
|
||||
/** 下发给 Webview 的报表数据。 */
|
||||
export type LocReportPayload = LocReport & LocReportHeader;
|
||||
|
||||
/** 宿主 → Webview 的消息。 */
|
||||
type ReportMessage =
|
||||
| { type: 'header'; payload: LocReportHeader }
|
||||
| { type: 'progress'; payload: LocScanProgress }
|
||||
| { type: 'report'; payload: LocReportPayload }
|
||||
| { type: 'error'; payload: { message: string } };
|
||||
|
||||
/** 两次进度上报之间的最小间隔(毫秒):文件多时不至于刷爆消息通道。 */
|
||||
const ProgressPostIntervalMs = 40;
|
||||
|
||||
export class LocReportView implements Disposable {
|
||||
private static currentPanel: LocReportView | undefined;
|
||||
|
||||
private readonly panel: WebviewPanel;
|
||||
private readonly listeners: Disposable[] = [];
|
||||
private scriptReady = false;
|
||||
private header: LocReportHeader | undefined;
|
||||
private progress: LocScanProgress | undefined;
|
||||
private payload: LocReportPayload | undefined;
|
||||
private lastProgressPostAt = 0;
|
||||
|
||||
private constructor(panel: WebviewPanel) {
|
||||
this.panel = panel;
|
||||
panel.webview.options = { enableScripts: true };
|
||||
panel.webview.html = BuildLocReportHtml(CreateNonce());
|
||||
|
||||
this.listeners.push(
|
||||
panel.webview.onDidReceiveMessage((message: { type?: string } | undefined) => {
|
||||
if (message?.type === 'ready') {
|
||||
this.scriptReady = true;
|
||||
// 脚本刚就绪:把已缓存的状态补齐,避免丢失 ready 之前的下发。
|
||||
this.PostHeader();
|
||||
this.PostProgressNow();
|
||||
this.PostReport();
|
||||
}
|
||||
}),
|
||||
panel.onDidDispose(() => this.Dispose())
|
||||
);
|
||||
}
|
||||
|
||||
/** 打开(或复用)报告面板。 */
|
||||
static Show(context: ExtensionContext): LocReportView {
|
||||
if (LocReportView.currentPanel) {
|
||||
LocReportView.currentPanel.panel.reveal();
|
||||
return LocReportView.currentPanel;
|
||||
}
|
||||
|
||||
const panel = window.createWebviewPanel(
|
||||
'viper.locReport',
|
||||
'Viper LOC',
|
||||
ViewColumn.Active,
|
||||
{ enableScripts: true, retainContextWhenHidden: true }
|
||||
);
|
||||
const view = new LocReportView(panel);
|
||||
LocReportView.currentPanel = view;
|
||||
context.subscriptions.push(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
/** 开始一轮统计:面板切回进度态并清空上一轮报表。 */
|
||||
StartRun(projectName: string, projectFile: string, sourceDir: string): void {
|
||||
this.header = { ProjectName: projectName, ProjectFile: projectFile, SourceDir: sourceDir };
|
||||
this.progress = { ProcessedFiles: 0, TotalFiles: 0, CurrentFile: '' };
|
||||
this.payload = undefined;
|
||||
this.panel.title = `Viper LOC · ${projectName}`;
|
||||
|
||||
this.PostHeader();
|
||||
this.PostProgressNow();
|
||||
}
|
||||
|
||||
/** 上报扫描进度(按间隔节流,但每轮最后一个文件必发)。 */
|
||||
ReportProgress(progress: LocScanProgress): void {
|
||||
this.progress = progress;
|
||||
const isLastFile = progress.TotalFiles > 0 && progress.ProcessedFiles >= progress.TotalFiles;
|
||||
if (!isLastFile && Date.now() - this.lastProgressPostAt < ProgressPostIntervalMs) {
|
||||
return;
|
||||
}
|
||||
this.PostProgressNow();
|
||||
}
|
||||
|
||||
/** 下发报表。 */
|
||||
ReportResult(payload: LocReportPayload): void {
|
||||
this.payload = payload;
|
||||
this.PostReport();
|
||||
}
|
||||
|
||||
/** 下发错误(源目录不存在等)。 */
|
||||
ReportError(message: string): void {
|
||||
this.Post({ type: 'error', payload: { message } });
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.Dispose();
|
||||
}
|
||||
|
||||
private Dispose(): void {
|
||||
if (LocReportView.currentPanel === this) {
|
||||
LocReportView.currentPanel = undefined;
|
||||
}
|
||||
for (const listener of this.listeners) {
|
||||
listener.dispose();
|
||||
}
|
||||
this.listeners.length = 0;
|
||||
this.scriptReady = false;
|
||||
}
|
||||
|
||||
private PostHeader(): void {
|
||||
if (this.header) {
|
||||
this.Post({ type: 'header', payload: this.header });
|
||||
}
|
||||
}
|
||||
|
||||
private PostProgressNow(): void {
|
||||
this.lastProgressPostAt = Date.now();
|
||||
if (this.progress) {
|
||||
this.Post({ type: 'progress', payload: this.progress });
|
||||
}
|
||||
}
|
||||
|
||||
private PostReport(): void {
|
||||
if (this.payload) {
|
||||
this.Post({ type: 'report', payload: this.payload });
|
||||
}
|
||||
}
|
||||
|
||||
private Post(message: ReportMessage): void {
|
||||
if (!this.scriptReady) {
|
||||
return;
|
||||
}
|
||||
void this.panel.webview.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成 Webview 脚本用的 nonce。 */
|
||||
function CreateNonce(): string {
|
||||
return randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
/** 面板骨架:静态 HTML + 内联样式 + 内联脚本(数据随后经 postMessage 下发)。 */
|
||||
function BuildLocReportHtml(nonce: string): string {
|
||||
const contentSecurityPolicy = [
|
||||
`default-src 'none'`,
|
||||
`style-src 'unsafe-inline'`,
|
||||
`script-src 'nonce-${nonce}'`,
|
||||
].join('; ');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="${contentSecurityPolicy}" />
|
||||
<title>Viper LOC</title>
|
||||
<style>
|
||||
:root {
|
||||
--seg-code: var(--vscode-charts-blue, #3794ff);
|
||||
--seg-comment: var(--vscode-charts-green, #89d185);
|
||||
--seg-blank: var(--vscode-charts-orange, #d18616);
|
||||
--line: var(--vscode-panel-border, rgba(128, 128, 128, 0.35));
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0 0 24px;
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-editor-background);
|
||||
font-family: var(--vscode-font-family);
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
header {
|
||||
padding: 14px 16px 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.35em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.meta {
|
||||
opacity: 0.75;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
section {
|
||||
padding: 14px 16px 0;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--vscode-editorWidget-background, rgba(128, 128, 128, 0.06));
|
||||
}
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.card-title .hint {
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
/* 进度条 */
|
||||
.bar {
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-input-background, rgba(128, 128, 128, 0.2));
|
||||
}
|
||||
#progress-fill {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: var(--seg-code);
|
||||
transition: width 90ms linear;
|
||||
}
|
||||
#progress-text {
|
||||
margin-top: 8px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
#progress-file {
|
||||
margin-top: 4px;
|
||||
opacity: 0.6;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* KPI */
|
||||
#kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.kpi {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.kpi-value {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.kpi-label {
|
||||
opacity: 0.7;
|
||||
margin-top: 2px;
|
||||
}
|
||||
/* 语言筛选 */
|
||||
#filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 18px;
|
||||
}
|
||||
.filter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.legend {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.swatch {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.swatch-code { background: var(--seg-code); }
|
||||
.swatch-comment { background: var(--seg-comment); }
|
||||
.swatch-blank { background: var(--seg-blank); }
|
||||
/* 行数构成图 */
|
||||
.chart-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(90px, 140px) 1fr minmax(140px, 200px);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 3px 0;
|
||||
}
|
||||
.chart-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chart-track {
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
background: var(--vscode-input-background, rgba(128, 128, 128, 0.16));
|
||||
}
|
||||
.chart-bar {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.seg { height: 100%; }
|
||||
.seg-code { background: var(--seg-code); }
|
||||
.seg-comment { background: var(--seg-comment); }
|
||||
.seg-blank { background: var(--seg-blank); }
|
||||
.chart-value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.85;
|
||||
text-align: right;
|
||||
}
|
||||
/* 明细表 */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
th, td {
|
||||
padding: 5px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--line);
|
||||
white-space: nowrap;
|
||||
}
|
||||
th.num, td.num { text-align: right; }
|
||||
thead th {
|
||||
opacity: 0.8;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
tbody tr:hover { background: var(--vscode-list-hoverBackground, rgba(128, 128, 128, 0.12)); }
|
||||
tr.total-row { font-weight: 600; }
|
||||
tr.total-row th, tr.total-row td { border-top: 1px solid var(--line); border-bottom: none; }
|
||||
.empty-row {
|
||||
opacity: 0.7;
|
||||
padding: 6px 0;
|
||||
}
|
||||
#empty, #error {
|
||||
padding: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
#error { color: var(--vscode-errorForeground); white-space: pre-wrap; }
|
||||
[hidden] { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1 id="title">Viper LOC 统计</h1>
|
||||
<div class="meta">
|
||||
<div id="meta-source"></div>
|
||||
<div id="meta-file"></div>
|
||||
<div id="meta-skipped"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="progress-card">
|
||||
<div class="card">
|
||||
<div class="card-title">正在统计<span class="hint">按已知语言递归扫描源目录</span></div>
|
||||
<div class="bar"><div id="progress-fill"></div></div>
|
||||
<div id="progress-text">正在收集文件…</div>
|
||||
<div id="progress-file"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="report" hidden>
|
||||
<div class="card">
|
||||
<div class="card-title">概览<span class="hint" id="kpi-scope"></span></div>
|
||||
<div id="kpis"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">语言筛选<span class="hint">勾选参与统计的语言(概览、图表与表格同步过滤)</span></div>
|
||||
<div id="filters"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
行数构成
|
||||
<span class="legend"><span class="swatch swatch-code"></span>代码</span>
|
||||
<span class="legend"><span class="swatch swatch-comment"></span>注释</span>
|
||||
<span class="legend"><span class="swatch swatch-blank"></span>空行</span>
|
||||
</div>
|
||||
<div id="chart"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">语言明细</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>语言</th>
|
||||
<th class="num">文件数</th>
|
||||
<th class="num">总行数</th>
|
||||
<th class="num">代码行</th>
|
||||
<th class="num">注释行</th>
|
||||
<th class="num">空行</th>
|
||||
<th class="num">注释率</th>
|
||||
<th class="num">平均行长</th>
|
||||
<th class="num">字符数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="empty" hidden>源目录下没有已知语言的文件。</div>
|
||||
<div id="error" hidden></div>
|
||||
|
||||
<script nonce="${nonce}">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var vscode = acquireVsCodeApi();
|
||||
var state = { header: null, progress: null, report: null, selected: {} };
|
||||
|
||||
function el(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
el(id).textContent = value === undefined || value === null ? '' : String(value);
|
||||
}
|
||||
|
||||
function num(value) {
|
||||
return Number(value || 0).toLocaleString('en-US');
|
||||
}
|
||||
|
||||
function pct(value) {
|
||||
return (value * 100).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
/** 注释率 = 注释行 /(代码行 + 注释行)。 */
|
||||
function commentRate(count) {
|
||||
var nonBlank = count.CodeLines + count.CommentLines;
|
||||
return nonBlank > 0 ? count.CommentLines / nonBlank : 0;
|
||||
}
|
||||
|
||||
/** 平均行字符数 = 字符数 / 总行数。 */
|
||||
function averageLineLength(count) {
|
||||
return count.TotalLines > 0 ? count.Chars / count.TotalLines : 0;
|
||||
}
|
||||
|
||||
function emptyCount() {
|
||||
return { FileCount: 0, TotalLines: 0, CodeLines: 0, CommentLines: 0, BlankLines: 0, Chars: 0 };
|
||||
}
|
||||
|
||||
function sumCounts(rows) {
|
||||
var total = emptyCount();
|
||||
rows.forEach(function (row) {
|
||||
total.FileCount += row.FileCount;
|
||||
total.TotalLines += row.TotalLines;
|
||||
total.CodeLines += row.CodeLines;
|
||||
total.CommentLines += row.CommentLines;
|
||||
total.BlankLines += row.BlankLines;
|
||||
total.Chars += row.Chars;
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
function makeNode(tag, className, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) {
|
||||
node.className = className;
|
||||
}
|
||||
if (text !== undefined) {
|
||||
node.textContent = text;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function setHidden(id, hidden) {
|
||||
if (hidden) {
|
||||
el(id).setAttribute('hidden', '');
|
||||
} else {
|
||||
el(id).removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
var header = state.header;
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
setText('title', 'LOC 统计 · ' + header.ProjectName);
|
||||
setText('meta-source', '项目 ' + header.ProjectName + ' · 源目录 ' + header.SourceDir);
|
||||
setText('meta-file', '工程文件 ' + header.ProjectFile);
|
||||
}
|
||||
|
||||
function renderProgress() {
|
||||
var progress = state.progress || { ProcessedFiles: 0, TotalFiles: 0, CurrentFile: '' };
|
||||
var ratio = progress.TotalFiles > 0 ? progress.ProcessedFiles / progress.TotalFiles : 0;
|
||||
el('progress-fill').style.width = (ratio * 100).toFixed(2) + '%';
|
||||
setText(
|
||||
'progress-text',
|
||||
progress.TotalFiles > 0
|
||||
? '已统计 ' + num(progress.ProcessedFiles) + ' / ' + num(progress.TotalFiles) + ' 个文件(' + pct(ratio) + ')'
|
||||
: '正在收集文件…'
|
||||
);
|
||||
setText('progress-file', progress.CurrentFile || '');
|
||||
}
|
||||
|
||||
function renderKpi(rows) {
|
||||
var totals = sumCounts(rows);
|
||||
var cards = [
|
||||
['文件数', num(totals.FileCount)],
|
||||
['总行数', num(totals.TotalLines)],
|
||||
['代码行', num(totals.CodeLines)],
|
||||
['注释行', num(totals.CommentLines)],
|
||||
['空行', num(totals.BlankLines)],
|
||||
['注释率', pct(commentRate(totals))],
|
||||
['平均行字符数', averageLineLength(totals).toFixed(1)],
|
||||
['字符总数', num(totals.Chars)]
|
||||
];
|
||||
|
||||
var host = el('kpis');
|
||||
host.textContent = '';
|
||||
cards.forEach(function (card) {
|
||||
var node = makeNode('div', 'kpi');
|
||||
node.appendChild(makeNode('div', 'kpi-value', card[1]));
|
||||
node.appendChild(makeNode('div', 'kpi-label', card[0]));
|
||||
host.appendChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
function renderFilters(languages) {
|
||||
var host = el('filters');
|
||||
host.textContent = '';
|
||||
languages.forEach(function (row) {
|
||||
var label = makeNode('label', 'filter');
|
||||
var input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = !!state.selected[row.Language];
|
||||
input.addEventListener('change', function () {
|
||||
state.selected[row.Language] = input.checked;
|
||||
renderReport();
|
||||
});
|
||||
label.appendChild(input);
|
||||
label.appendChild(document.createTextNode(row.Language + '(' + num(row.FileCount) + ' 个文件)'));
|
||||
host.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
function renderChart(rows) {
|
||||
var host = el('chart');
|
||||
host.textContent = '';
|
||||
if (rows.length === 0) {
|
||||
host.appendChild(makeNode('div', 'empty-row', '未选择任何语言'));
|
||||
return;
|
||||
}
|
||||
|
||||
var maxTotal = 1;
|
||||
rows.forEach(function (row) {
|
||||
maxTotal = Math.max(maxTotal, row.TotalLines);
|
||||
});
|
||||
|
||||
rows.forEach(function (row) {
|
||||
var line = makeNode('div', 'chart-row');
|
||||
line.appendChild(makeNode('div', 'chart-label', row.Language));
|
||||
|
||||
var track = makeNode('div', 'chart-track');
|
||||
var bar = makeNode('div', 'chart-bar');
|
||||
bar.style.width = ((row.TotalLines / maxTotal) * 100).toFixed(2) + '%';
|
||||
[
|
||||
['code', row.CodeLines],
|
||||
['comment', row.CommentLines],
|
||||
['blank', row.BlankLines]
|
||||
].forEach(function (part) {
|
||||
var segment = makeNode('div', 'seg seg-' + part[0]);
|
||||
segment.style.width =
|
||||
row.TotalLines > 0 ? ((part[1] / row.TotalLines) * 100).toFixed(2) + '%' : '0%';
|
||||
bar.appendChild(segment);
|
||||
});
|
||||
track.appendChild(bar);
|
||||
line.appendChild(track);
|
||||
|
||||
line.appendChild(
|
||||
makeNode('div', 'chart-value', num(row.CodeLines) + ' 代码 / ' + num(row.TotalLines) + ' 行')
|
||||
);
|
||||
host.appendChild(line);
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable(rows, totalRow) {
|
||||
var host = el('table-body');
|
||||
host.textContent = '';
|
||||
|
||||
var appendRow = function (row, label, isTotal) {
|
||||
var tr = document.createElement('tr');
|
||||
if (isTotal) {
|
||||
tr.className = 'total-row';
|
||||
}
|
||||
[
|
||||
label,
|
||||
num(row.FileCount),
|
||||
num(row.TotalLines),
|
||||
num(row.CodeLines),
|
||||
num(row.CommentLines),
|
||||
num(row.BlankLines),
|
||||
pct(commentRate(row)),
|
||||
averageLineLength(row).toFixed(1),
|
||||
num(row.Chars)
|
||||
].forEach(function (value, index) {
|
||||
var cell = document.createElement(index === 0 ? 'th' : 'td');
|
||||
cell.textContent = value;
|
||||
if (index > 0) {
|
||||
cell.className = 'num';
|
||||
}
|
||||
tr.appendChild(cell);
|
||||
});
|
||||
host.appendChild(tr);
|
||||
};
|
||||
|
||||
rows.forEach(function (row) {
|
||||
appendRow(row, row.Language, false);
|
||||
});
|
||||
appendRow(totalRow, '合计', true);
|
||||
}
|
||||
|
||||
function renderReport() {
|
||||
var report = state.report;
|
||||
if (!report) {
|
||||
return;
|
||||
}
|
||||
|
||||
report.Languages.forEach(function (row) {
|
||||
if (state.selected[row.Language] === undefined) {
|
||||
state.selected[row.Language] = true;
|
||||
}
|
||||
});
|
||||
|
||||
var selectedRows = report.Languages.filter(function (row) {
|
||||
return state.selected[row.Language];
|
||||
});
|
||||
|
||||
setText('meta-skipped', '跳过目录 ' + (report.SkippedDirectories.length > 0 ? report.SkippedDirectories.join('、') : '(无)'));
|
||||
setText('kpi-scope', selectedRows.length === report.Languages.length ? '全部语言' : '已选 ' + selectedRows.length + ' / ' + report.Languages.length + ' 种语言');
|
||||
|
||||
renderKpi(selectedRows);
|
||||
renderFilters(report.Languages);
|
||||
renderChart(selectedRows);
|
||||
renderTable(selectedRows, Object.assign({ Language: 'Total' }, sumCounts(selectedRows)));
|
||||
|
||||
setHidden('progress-card', true);
|
||||
setHidden('report', false);
|
||||
setHidden('empty', report.Languages.length > 0);
|
||||
setHidden('error', true);
|
||||
}
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
var message = event.data;
|
||||
if (!message || !message.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'header') {
|
||||
state.header = message.payload || null;
|
||||
state.progress = { ProcessedFiles: 0, TotalFiles: 0, CurrentFile: '' };
|
||||
state.report = null;
|
||||
state.selected = {};
|
||||
renderHeader();
|
||||
setHidden('progress-card', false);
|
||||
setHidden('report', true);
|
||||
setHidden('empty', true);
|
||||
setHidden('error', true);
|
||||
renderProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'progress') {
|
||||
state.progress = message.payload;
|
||||
renderProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'report') {
|
||||
state.report = message.payload;
|
||||
state.selected = {};
|
||||
renderReport();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'error') {
|
||||
setText('error', message.payload && message.payload.message ? message.payload.message : '统计失败');
|
||||
setHidden('progress-card', true);
|
||||
setHidden('report', true);
|
||||
setHidden('empty', true);
|
||||
setHidden('error', false);
|
||||
}
|
||||
});
|
||||
|
||||
vscode.postMessage({ type: 'ready' });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
Reference in new issue
Block a user