diff --git a/src/extension.ts b/src/extension.ts index c2d66f9..ecd13b5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import * as cp from "child_process"; import { throttle } from "throttle-debounce"; +import { fetchFileBlame, CommitInfo } from "./git"; import { parseGitBlamePorcelain, @@ -15,11 +16,14 @@ const decorationType = vscode.window.createTextEditorDecorationType({ }, }); +const FileCommits: Map = new Map(); + function showDecoration(e: { readonly textEditor: vscode.TextEditor }) { const editor = e.textEditor; const document = editor.document; const activeLine = document.lineAt(editor.selection.active.line); const { uri: file, isDirty } = document; + const command = "git"; const n = activeLine.lineNumber; @@ -29,6 +33,17 @@ function showDecoration(e: { readonly textEditor: vscode.TextEditor }) { args.push("--content", "-"); } + + const commitInfos = FileCommits.get(file.fsPath); + if (commitInfos !== undefined) { + const thiscommitInfo = commitInfos.find(info => info.lineNumber === n); + // TODO: get values of fields from thiscommitInfo + + } + else { + console.log(`No pre-fetched commit information found for ${file.fsPath}`); + // TODO: move obsolete logic from down below here if the commitInfos is not found + } const workspaceFolder = vscode.workspace.getWorkspaceFolder(file); const workspaceFolderPath = workspaceFolder?.uri.fsPath; const options = { cwd: workspaceFolderPath }; @@ -73,12 +88,41 @@ function showDecoration(e: { readonly textEditor: vscode.TextEditor }) { }); } +function showFileBlame(e: { readonly textEditor: vscode.TextEditor }) { + + const editor = e.textEditor; + const document = editor.document; + const { uri: file, isDirty } = document; + + fetchFileBlame(file.fsPath) + .then(commitInfos => { + console.log(`Commit Information for ${file.fsPath}`); + console.log(commitInfos); + FileCommits.set(file.fsPath, commitInfos); + }) + +} export function activate(context: vscode.ExtensionContext) { console.log('Extension "git-line-blame" has activated.'); let showDecorationThrottled = throttle(100, showDecoration); context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection(showDecorationThrottled), vscode.window.onDidChangeTextEditorVisibleRanges(showDecorationThrottled), + vscode.window.onDidChangeActiveTextEditor((e) => { + const editor = vscode.window.activeTextEditor; + if (editor !== undefined && e === editor) { + showFileBlame({ textEditor: editor }) + } + }), + vscode.window.onDidChangeVisibleTextEditors(editors => { + const closedEditors = vscode.window.visibleTextEditors.filter(editor => + !editors.includes(editor) + ); + closedEditors.forEach(closedEditor => { + console.log(`Closed file: ${closedEditor.document.fileName}`); + FileCommits.delete(closedEditor.document.fileName); + }); + }), vscode.workspace.onDidSaveTextDocument((e) => { const editor = vscode.window.activeTextEditor; if (editor !== undefined && e === editor.document) { diff --git a/src/git.ts b/src/git.ts new file mode 100644 index 0000000..af5b2f7 --- /dev/null +++ b/src/git.ts @@ -0,0 +1,93 @@ +import { exec } from 'child_process'; + +export interface CommitInfo { + commitHash: string; + author: string; + lineNumber: number; + commitTitle: string; +} + +export function fetchFileBlame(file: string): Promise { + return new Promise((resolve, reject) => { + // Run git blame + exec(`git blame -w ${file}`, async (error, stdout, stderr) => { + if (error) { + console.error(`Error executing git blame: ${error.message}`); + reject(error.message); + return; + } + + const commitInfos: CommitInfo[] = []; + const commitHashes = new Set(); + + stdout.split('\n').forEach(line => { + if (line.trim() === '') { + return; + } + + const match = line.match(/^([^\s]+) \(([^)]+)\s+(\d+)\) .*/); + if (match) { + const commitHash = match[1]; + const authorInfo = match[2]; + const lineNumber = parseInt(match[3], 10); + + // Extract the author's name from authorInfo + const authorName = authorInfo.split(' ').slice(0, -3).join(' '); + + commitHashes.add(commitHash); + + commitInfos.push({ + commitHash, + author: authorName, + lineNumber, + commitTitle: '', // Placeholder for commit title + }); + } else { + console.warn(`Unexpected format for line: ${line}`); + } + }); + + // Fetch commit titles for the found commit hashes + try { + const commitTitles = await fetchCommitTitles(Array.from(commitHashes)); + + // Assign commit titles to commitInfos + commitInfos.forEach(info => { + if (commitTitles.has(info.commitHash)) { + info.commitTitle = commitTitles.get(info.commitHash) || ''; + } + }); + + resolve(commitInfos); + } catch (fetchError) { + reject(`Error fetching commit titles: ${fetchError}`); + } + }); + }); +} + +// Function to fetch commit titles from commit hashes using git log + function fetchCommitTitles(commitHashes: string[]): Promise> { + return new Promise>((resolve, reject) => { + const gitLogCommand = `git log --format="%H %s" ${commitHashes.join(' ')}`; + + exec(gitLogCommand, (error, stdout, stderr) => { + if (error) { + reject(`Error executing git log: ${error.message}`); + return; + } + + const commitTitles = new Map(); + + stdout.split('\n').forEach(line => { + if (line.trim() !== '') { + const [commitHash, ...titleParts] = line.trim().split(' '); + const commitTitle = titleParts.join(' '); + commitTitles.set(commitHash, commitTitle); + } + }); + + resolve(commitTitles); + }); + }); +}