From 880565ee5a8107bda16e0ca77e00bc8a2dc6bb5d Mon Sep 17 00:00:00 2001 From: lovr Date: Wed, 11 Mar 2026 16:56:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B9=B6=E5=8F=91=E5=88=86=E6=9E=90=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E9=87=8F=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/benchmark-performance.mjs | 240 ++++++++++++++++++++++++++++++ src/cacheManager.ts | 123 +++++++++------ src/extension.ts | 38 +++-- src/fileTree.ts | 188 +++++++++++++++++++++-- src/treeDataProvider.ts | 2 +- 5 files changed, 526 insertions(+), 65 deletions(-) create mode 100644 scripts/benchmark-performance.mjs diff --git a/scripts/benchmark-performance.mjs b/scripts/benchmark-performance.mjs new file mode 100644 index 0000000..721fb72 --- /dev/null +++ b/scripts/benchmark-performance.mjs @@ -0,0 +1,240 @@ +import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as path from 'path'; +import { execFileSync } from 'child_process'; +import { performance } from 'perf_hooks'; +import { fileURLToPath } from 'url'; +import Module from 'module'; +import ts from 'typescript'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const baselineRef = process.argv[2] || 'upstream/main'; + +function runGit(cwd, args, env = {}) { + return execFileSync('git', args, { + cwd, + encoding: 'utf8', + env: { ...process.env, ...env } + }); +} + +function loadFileTreeModule(sourceText, label) { + const patchedSource = sourceText.replace( + /import\s+\*\s+as\s+vscode\s+from\s+['"]vscode['"];?/, + 'const vscode = {};' + ); + const transpiled = ts.transpileModule(patchedSource, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2020, + esModuleInterop: true + }, + fileName: `${label}.ts` + }); + + const outputPath = path.join(repoRoot, '.bench-temp', `${label}.cjs`); + const mod = new Module(outputPath); + mod.filename = outputPath; + mod.paths = Module._nodeModulePaths(repoRoot); + mod._compile(transpiled.outputText, outputPath); + return mod.exports; +} + +async function prepareModuleSources() { + const currentSource = await fs.readFile(path.join(repoRoot, 'src', 'fileTree.ts'), 'utf8'); + const baselineSource = runGit(repoRoot, ['show', `${baselineRef}:src/fileTree.ts`]); + return { + current: loadFileTreeModule(currentSource, 'current-fileTree'), + baseline: loadFileTreeModule(baselineSource, 'baseline-fileTree') + }; +} + +async function ensureBenchTempDir() { + await fs.mkdir(path.join(repoRoot, '.bench-temp'), { recursive: true }); +} + +function makeFileLines(fileIndex, lineCount) { + return Array.from({ length: lineCount }, (_, lineIndex) => { + const lineNo = lineIndex + 1; + return `export const value_${fileIndex}_${lineNo} = '${fileIndex}:${lineNo}:seed';`; + }); +} + +async function writeRepoFiles(repoPath, files) { + await Promise.all( + files.map(file => + fs + .mkdir(path.dirname(path.join(repoPath, file.relativePath)), { recursive: true }) + .then(() => + fs.writeFile(path.join(repoPath, file.relativePath), `${file.lines.join('\n')}\n`, 'utf8') + ) + ) + ); +} + +async function createSyntheticRepo() { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'code-kingdom-bench-')); + const repoPath = path.join(tempRoot, 'target-repo'); + await fs.mkdir(repoPath, { recursive: true }); + + runGit(repoPath, ['init', '-b', 'main']); + + const fileCount = 120; + const lineCount = 240; + const files = Array.from({ length: fileCount }, (_, index) => ({ + relativePath: path.join('packages', `pkg-${index % 8}`, `module-${index}.ts`), + lines: makeFileLines(index, lineCount) + })); + + await writeRepoFiles(repoPath, files); + runGit(repoPath, ['add', '.']); + runGit(repoPath, ['commit', '-m', 'initial import'], { + GIT_AUTHOR_NAME: 'Alice', + GIT_AUTHOR_EMAIL: 'alice@example.com', + GIT_COMMITTER_NAME: 'Alice', + GIT_COMMITTER_EMAIL: 'alice@example.com' + }); + + for (const file of files) { + for (let i = 0; i < 40; i++) { + file.lines[i] = `export const value_bob_${i} = '${file.relativePath}:${i}:bob';`; + } + } + await writeRepoFiles(repoPath, files); + runGit(repoPath, ['add', '.']); + runGit(repoPath, ['commit', '-m', 'bob touches headers'], { + GIT_AUTHOR_NAME: 'Bob', + GIT_AUTHOR_EMAIL: 'bob@example.com', + GIT_COMMITTER_NAME: 'Bob', + GIT_COMMITTER_EMAIL: 'bob@example.com' + }); + + for (let fileIndex = 0; fileIndex < files.length; fileIndex++) { + if (fileIndex % 2 !== 0) { + continue; + } + for (let i = 100; i < 150; i++) { + files[fileIndex].lines[i] = `export const value_carol_${i} = '${files[fileIndex].relativePath}:${i}:carol';`; + } + } + await writeRepoFiles(repoPath, files); + runGit(repoPath, ['add', '.']); + runGit(repoPath, ['commit', '-m', 'carol refactors middle sections'], { + GIT_AUTHOR_NAME: 'Carol', + GIT_AUTHOR_EMAIL: 'carol@example.com', + GIT_COMMITTER_NAME: 'Carol', + GIT_COMMITTER_EMAIL: 'carol@example.com' + }); + + for (let fileIndex = 0; fileIndex < files.length; fileIndex++) { + if (fileIndex % 3 !== 0) { + continue; + } + for (let i = 180; i < 220; i++) { + files[fileIndex].lines[i] = `export const value_dave_${i} = '${files[fileIndex].relativePath}:${i}:dave';`; + } + } + await writeRepoFiles(repoPath, files); + runGit(repoPath, ['add', '.']); + runGit(repoPath, ['commit', '-m', 'dave edits tail sections'], { + GIT_AUTHOR_NAME: 'Dave', + GIT_AUTHOR_EMAIL: 'dave@example.com', + GIT_COMMITTER_NAME: 'Dave', + GIT_COMMITTER_EMAIL: 'dave@example.com' + }); + + return { repoPath, files }; +} + +async function analyze(moduleApi, repoPath, cache) { + const workspaceFolder = { + uri: { fsPath: repoPath }, + name: path.basename(repoPath) + }; + + const buildStart = performance.now(); + const tree = await moduleApi.buildFileTree(workspaceFolder); + const buildMs = performance.now() - buildStart; + if (!tree) { + throw new Error('buildFileTree returned null'); + } + + const blameStart = performance.now(); + let nextCache = cache || {}; + if (typeof moduleApi.getGitFileInventory === 'function') { + const inventory = await moduleApi.getGitFileInventory(repoPath); + nextCache = await moduleApi.addBlameInfo(tree, repoPath, { + fileCache: cache || {}, + trackedBlobHashes: inventory.trackedBlobHashes, + dirtyPaths: inventory.dirtyPaths + }); + } else { + await moduleApi.addBlameInfo(tree, repoPath); + } + const blameMs = performance.now() - blameStart; + + return { + cache: nextCache, + buildMs, + blameMs, + totalMs: buildMs + blameMs + }; +} + +async function dirtySmallSubset(repoPath, files) { + const dirtyTargets = files.slice(0, 3); + for (const file of dirtyTargets) { + for (let i = 10; i < 20; i++) { + file.lines[i] = `export const dirty_${i} = '${file.relativePath}:${i}:dirty';`; + } + } + await writeRepoFiles(repoPath, dirtyTargets); +} + +function formatMs(ms) { + return `${ms.toFixed(1)} ms`; +} + +function speedup(before, after) { + return `${(before / after).toFixed(2)}x`; +} + +async function main() { + await ensureBenchTempDir(); + const modules = await prepareModuleSources(); + const baselineColdTarget = await createSyntheticRepo(); + const currentColdTarget = await createSyntheticRepo(); + const baselineDirtyTarget = await createSyntheticRepo(); + const currentDirtyTarget = await createSyntheticRepo(); + + const baselineCold = await analyze(modules.baseline, baselineColdTarget.repoPath); + const currentCold = await analyze(modules.current, currentColdTarget.repoPath, {}); + + await analyze(modules.baseline, baselineDirtyTarget.repoPath); + await dirtySmallSubset(baselineDirtyTarget.repoPath, baselineDirtyTarget.files); + const baselineDirty = await analyze(modules.baseline, baselineDirtyTarget.repoPath); + + const currentPrime = await analyze(modules.current, currentDirtyTarget.repoPath, {}); + await dirtySmallSubset(currentDirtyTarget.repoPath, currentDirtyTarget.files); + const currentDirty = await analyze(modules.current, currentDirtyTarget.repoPath, currentPrime.cache); + + console.log(`Synthetic repo example: ${currentColdTarget.repoPath}`); + console.log(`Baseline ref: ${baselineRef}`); + console.log(''); + console.log('Cold run'); + console.log(` baseline total: ${formatMs(baselineCold.totalMs)} (build ${formatMs(baselineCold.buildMs)}, blame ${formatMs(baselineCold.blameMs)})`); + console.log(` current total: ${formatMs(currentCold.totalMs)} (build ${formatMs(currentCold.buildMs)}, blame ${formatMs(currentCold.blameMs)})`); + console.log(` speedup: ${speedup(baselineCold.totalMs, currentCold.totalMs)}`); + console.log(''); + console.log('Dirty rerun after editing 3 tracked files'); + console.log(` baseline total: ${formatMs(baselineDirty.totalMs)} (build ${formatMs(baselineDirty.buildMs)}, blame ${formatMs(baselineDirty.blameMs)})`); + console.log(` current total: ${formatMs(currentDirty.totalMs)} (build ${formatMs(currentDirty.buildMs)}, blame ${formatMs(currentDirty.blameMs)})`); + console.log(` speedup: ${speedup(baselineDirty.totalMs, currentDirty.totalMs)}`); +} + +main().catch(error => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/cacheManager.ts b/src/cacheManager.ts index 98cc948..6bdb209 100644 --- a/src/cacheManager.ts +++ b/src/cacheManager.ts @@ -1,47 +1,82 @@ -import * as vscode from "vscode"; -import * as fs from "fs"; -import * as path from "path"; -import { FileNode } from "./fileTree"; +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import type { BlameSegment, FileNode } from './fileTree'; + +export type FileBlameCacheEntry = { + signature: string; + blameSegments: BlameSegment[] | null; +}; + +export type FileBlameCacheStore = Record; export class CacheManager { - private storageUri: vscode.Uri; - - constructor(context: vscode.ExtensionContext) { - if (!context.storageUri) { - throw new Error("Workspace storage is not available"); - } - this.storageUri = context.storageUri; - this.ensureStorageDir(); - } - - private async ensureStorageDir() { - try { - await fs.promises.mkdir(this.storageUri.fsPath, { recursive: true }); - } catch (error) { - console.error("Failed to create storage directory:", error); - } - } - - private getCacheFilePath(hash: string): string { - return path.join(this.storageUri.fsPath, `blame_cache_${hash}.json`); - } - - async getBlameCache(hash: string): Promise { - const filePath = this.getCacheFilePath(hash); - try { - const content = await fs.promises.readFile(filePath, "utf-8"); - return JSON.parse(content) as FileNode; - } catch { - return null; - } - } - - async saveBlameCache(hash: string, data: FileNode): Promise { - const filePath = this.getCacheFilePath(hash); - try { - await fs.promises.writeFile(filePath, JSON.stringify(data), "utf-8"); - } catch (error) { - console.error("Failed to save blame cache:", error); - } - } + private readonly storageUri: vscode.Uri; + private readonly storageReady: Promise; + + constructor(context: vscode.ExtensionContext) { + if (!context.storageUri) { + throw new Error('Workspace storage is not available'); + } + this.storageUri = context.storageUri; + this.storageReady = this.ensureStorageDir(); + } + + private async ensureStorageDir(): Promise { + try { + await fs.promises.mkdir(this.storageUri.fsPath, { recursive: true }); + } catch (error) { + console.error('Failed to create storage directory:', error); + } + } + + private getSnapshotCacheFilePath(hash: string): string { + return path.join(this.storageUri.fsPath, `blame_cache_${hash}.json`); + } + + private getFileCacheFilePath(): string { + return path.join(this.storageUri.fsPath, 'blame_file_cache.json'); + } + + async getBlameCache(hash: string): Promise { + await this.storageReady; + const filePath = this.getSnapshotCacheFilePath(hash); + try { + const content = await fs.promises.readFile(filePath, 'utf-8'); + return JSON.parse(content) as FileNode; + } catch { + return null; + } + } + + async saveBlameCache(hash: string, data: FileNode): Promise { + await this.storageReady; + const filePath = this.getSnapshotCacheFilePath(hash); + try { + await fs.promises.writeFile(filePath, JSON.stringify(data), 'utf-8'); + } catch (error) { + console.error('Failed to save blame cache:', error); + } + } + + async getFileBlameCache(): Promise { + await this.storageReady; + const filePath = this.getFileCacheFilePath(); + try { + const content = await fs.promises.readFile(filePath, 'utf-8'); + return JSON.parse(content) as FileBlameCacheStore; + } catch { + return {}; + } + } + + async saveFileBlameCache(cache: FileBlameCacheStore): Promise { + await this.storageReady; + const filePath = this.getFileCacheFilePath(); + try { + await fs.promises.writeFile(filePath, JSON.stringify(cache), 'utf-8'); + } catch (error) { + console.error('Failed to save file blame cache:', error); + } + } } diff --git a/src/extension.ts b/src/extension.ts index 230bd8a..b55a850 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { addBlameInfo, buildFileTree, collectAuthors, getCurrentCommitHash, isGitRepository, type AuthorColorMap, type FileNode } from './fileTree'; +import { addBlameInfo, buildFileTree, collectAuthors, getCurrentCommitHash, getGitFileInventory, isGitRepository, type AuthorColorMap, type FileNode } from './fileTree'; import { CacheManager } from './cacheManager'; import { getWebviewContent } from './webviewContent'; import { CodeKingdomTreeDataProvider } from './treeDataProvider'; @@ -55,15 +55,19 @@ export function activate(context: vscode.ExtensionContext) { try { // 获取当前 commit hash const commitHash = await getCurrentCommitHash(repoRoot); + const gitFileInventory = await getGitFileInventory(repoRoot); + const hasDirtyFiles = gitFileInventory.dirtyPaths.size > 0; let fileTree: FileNode | null = null; let isCached = false; - if (commitHash) { + if (commitHash && !hasDirtyFiles) { fileTree = await cacheManager.getBlameCache(commitHash); if (fileTree) { isCached = true; console.log(`Using cached blame data for commit ${commitHash}`); } + } else if (commitHash) { + console.log('Skipping full blame cache because the working tree has uncommitted changes.'); } if (isCached && fileTree) { @@ -79,10 +83,11 @@ export function activate(context: vscode.ExtensionContext) { panel.webview.onDidReceiveMessage( message => { switch (message.command) { - case 'openFile': + case 'openFile': { const fileUri = vscode.Uri.file(message.path); vscode.window.showTextDocument(fileUri); break; + } } }, undefined, @@ -106,15 +111,24 @@ export function activate(context: vscode.ExtensionContext) { return; } + progress.report({ message: '正在加载 blame 缓存...' }); + const fileBlameCache = await cacheManager.getFileBlameCache(); + progress.report({ message: '正在计算 Git blame...' }); - await addBlameInfo(fileTree, repoRoot, (done, total, filePath) => { - const percent = total > 0 ? Math.round((done / total) * 100) : 0; - const name = filePath.split(/[\\/]/).pop() || filePath; - progress.report({ message: `Git blame ${percent}% - ${name}` }); + const nextFileBlameCache = await addBlameInfo(fileTree, repoRoot, { + fileCache: fileBlameCache, + trackedBlobHashes: gitFileInventory.trackedBlobHashes, + dirtyPaths: gitFileInventory.dirtyPaths, + onProgress: (done, total, filePath) => { + const percent = total > 0 ? Math.round((done / total) * 100) : 0; + const name = filePath.split(/[\\/]/).pop() || filePath; + progress.report({ message: `Git blame ${percent}% - ${name}` }); + } }); + await cacheManager.saveFileBlameCache(nextFileBlameCache); // 存入缓存 - if (commitHash && fileTree) { + if (commitHash && fileTree && !hasDirtyFiles) { await cacheManager.saveBlameCache(commitHash, fileTree); } @@ -264,8 +278,12 @@ function sanitizeColorMap(input: unknown): AuthorColorMap { } const result: AuthorColorMap = {}; for (const [key, value] of Object.entries(input as Record)) { - if (typeof key !== 'string') continue; - if (typeof value !== 'string') continue; + if (typeof key !== 'string') { + continue; + } + if (typeof value !== 'string') { + continue; + } result[key] = value; } return result; diff --git a/src/fileTree.ts b/src/fileTree.ts index 7a14c18..fd7f532 100644 --- a/src/fileTree.ts +++ b/src/fileTree.ts @@ -1,11 +1,17 @@ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import ignore, { type Ignore } from 'ignore'; import { execFile } from 'child_process'; import { promisify } from 'util'; +import type { FileBlameCacheStore } from './cacheManager'; const execFileAsync = promisify(execFile); +const DEFAULT_BLAME_CONCURRENCY = Math.min( + 6, + Math.max(2, typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length) +); export type BlameSegment = { author: string; @@ -14,6 +20,11 @@ export type BlameSegment = { export type AuthorColorMap = Record; +export type GitFileInventory = { + trackedBlobHashes: Map; + dirtyPaths: Set; +}; + export interface FileNode { name: string; path: string; @@ -214,7 +225,9 @@ function collectFileNodes(root: FileNode): FileNode[] { const stack: FileNode[] = [root]; while (stack.length > 0) { const node = stack.pop(); - if (!node) continue; + if (!node) { + continue; + } if (node.type === 'file') { files.push(node); } else if (node.children) { @@ -226,27 +239,180 @@ function collectFileNodes(root: FileNode): FileNode[] { return files; } +function toRepoRelativePath(repoRoot: string, filePath: string): string { + return path.relative(repoRoot, filePath).replace(/\\/g, '/'); +} + +async function getTrackedBlobHashes(repoRoot: string): Promise> { + try { + const { stdout } = await execFileAsync('git', ['-C', repoRoot, 'ls-files', '-s', '-z', '--']); + const hashes = new Map(); + + for (const entry of stdout.split('\0')) { + if (!entry) { + continue; + } + + const tabIndex = entry.indexOf('\t'); + if (tabIndex === -1) { + continue; + } + + const metadata = entry.slice(0, tabIndex).trim().split(/\s+/); + const blobHash = metadata[1]; + const relativePath = entry.slice(tabIndex + 1); + if (!blobHash || !relativePath) { + continue; + } + + hashes.set(path.join(repoRoot, relativePath), blobHash); + } + + return hashes; + } catch { + return new Map(); + } +} + +async function readGitPathSet(repoRoot: string, args: string[]): Promise> { + try { + const { stdout } = await execFileAsync('git', ['-C', repoRoot, ...args]); + const paths = new Set(); + for (const relativePath of stdout.split('\0')) { + if (!relativePath) { + continue; + } + paths.add(path.join(repoRoot, relativePath)); + } + return paths; + } catch { + return new Set(); + } +} + +export async function getGitFileInventory(repoRoot: string): Promise { + const [trackedBlobHashes, unstagedPaths, stagedPaths, untrackedPaths] = await Promise.all([ + getTrackedBlobHashes(repoRoot), + readGitPathSet(repoRoot, ['diff', '--name-only', '-z', '--']), + readGitPathSet(repoRoot, ['diff', '--cached', '--name-only', '-z', '--']), + readGitPathSet(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z', '--']) + ]); + + const dirtyPaths = new Set(); + for (const filePath of unstagedPaths) { + dirtyPaths.add(filePath); + } + for (const filePath of stagedPaths) { + dirtyPaths.add(filePath); + } + for (const filePath of untrackedPaths) { + dirtyPaths.add(filePath); + } + + return { trackedBlobHashes, dirtyPaths }; +} + +async function getFileCacheSignature( + filePath: string, + trackedBlobHashes: Map, + dirtyPaths: Set +): Promise { + const blobHash = trackedBlobHashes.get(filePath); + if (blobHash && !dirtyPaths.has(filePath)) { + return `git:${blobHash}`; + } + + const stat = await fs.promises.stat(filePath); + return `fs:${stat.size}:${Math.trunc(stat.mtimeMs)}`; +} + +async function runWithConcurrency(tasks: Array<() => Promise>, concurrency: number): Promise { + let nextIndex = 0; + const workerCount = Math.min(Math.max(1, concurrency), tasks.length); + const workers = Array.from({ length: workerCount }, async () => { + while (nextIndex < tasks.length) { + const taskIndex = nextIndex; + nextIndex++; + await tasks[taskIndex](); + } + }); + await Promise.all(workers); +} + +type AddBlameInfoOptions = { + onProgress?: (done: number, total: number, filePath: string) => void; + fileCache?: FileBlameCacheStore; + trackedBlobHashes?: Map; + dirtyPaths?: Set; + concurrency?: number; +}; + export async function addBlameInfo( root: FileNode, repoRoot: string, - onProgress?: (done: number, total: number, filePath: string) => void -): Promise { + options: AddBlameInfoOptions = {} +): Promise { const files = collectFileNodes(root); const total = files.length; let done = 0; + const onProgress = options.onProgress; + const trackedBlobHashes = options.trackedBlobHashes || new Map(); + const dirtyPaths = options.dirtyPaths || new Set(); + const hasTrackedInventory = trackedBlobHashes.size > 0; + const inputCache = options.fileCache || {}; + const nextCache: FileBlameCacheStore = {}; + const blameTasks: Array<() => Promise> = []; for (const file of files) { - done++; - onProgress?.(done, total, file.path); - if (!file.isText) { + done++; + onProgress?.(done, total, file.path); + continue; + } + + const relativePath = toRepoRelativePath(repoRoot, file.path); + let signature: string; + try { + signature = await getFileCacheSignature(file.path, trackedBlobHashes, dirtyPaths); + } catch { + done++; + onProgress?.(done, total, file.path); + continue; + } + const cachedEntry = inputCache[relativePath]; + if (cachedEntry && cachedEntry.signature === signature) { + nextCache[relativePath] = cachedEntry; + if (cachedEntry.blameSegments && cachedEntry.blameSegments.length > 0) { + file.blameSegments = cachedEntry.blameSegments; + } + done++; + onProgress?.(done, total, file.path); continue; } - const segments = await getBlameSegments(file.path, repoRoot); - if (segments && segments.length > 0) { - file.blameSegments = segments; + + if (hasTrackedInventory && !trackedBlobHashes.has(file.path)) { + nextCache[relativePath] = { signature, blameSegments: null }; + done++; + onProgress?.(done, total, file.path); + continue; } + + blameTasks.push(async () => { + const segments = await getBlameSegments(file.path, repoRoot); + if (segments && segments.length > 0) { + file.blameSegments = segments; + } + nextCache[relativePath] = { + signature, + blameSegments: segments && segments.length > 0 ? segments : null + }; + done++; + onProgress?.(done, total, file.path); + }); } + + await runWithConcurrency(blameTasks, options.concurrency ?? DEFAULT_BLAME_CONCURRENCY); + return nextCache; } export function collectAuthors(root: FileNode): string[] { @@ -254,7 +420,9 @@ export function collectAuthors(root: FileNode): string[] { const stack: FileNode[] = [root]; while (stack.length > 0) { const node = stack.pop(); - if (!node) continue; + if (!node) { + continue; + } if (node.type === 'file' && node.blameSegments) { for (const seg of node.blameSegments) { if (seg.author) { diff --git a/src/treeDataProvider.ts b/src/treeDataProvider.ts index fdf0cd0..f860b3a 100644 --- a/src/treeDataProvider.ts +++ b/src/treeDataProvider.ts @@ -12,7 +12,7 @@ export class CodeKingdomTreeDataProvider implements vscode.TreeDataProvider { + getChildren(): Thenable { // 返回按钮项 const mapItem = new vscode.TreeItem('显示开发人员势力图', vscode.TreeItemCollapsibleState.None); mapItem.command = {