From f625d37b008514cb33403503a404a5c32fbe9bc7 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 20 Apr 2019 02:09:50 -0400 Subject: [PATCH] Improves changes hover accuracy & diff rendering Adds hovers.changesDiff setting for line vs hunk diff --- README.md | 1 + package.json | 14 ++++ src/annotations/annotations.ts | 58 +++++++++------ src/annotations/blameAnnotationProvider.ts | 7 +- src/annotations/gutterBlameAnnotationProvider.ts | 9 +-- src/annotations/heatmapBlameAnnotationProvider.ts | 7 +- src/annotations/recentChangesAnnotationProvider.ts | 13 ++-- src/commands/diffLineWithWorking.ts | 3 +- src/config.ts | 1 + src/git/git.ts | 6 +- src/git/gitService.ts | 81 ++++++++++++--------- src/git/models/diff.ts | 24 +++---- src/git/parsers/blameParser.ts | 6 +- src/git/parsers/diffParser.ts | 82 ++++++++++------------ src/trackers/gitLineTracker.ts | 2 +- 15 files changed, 179 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index be88105..cc45f40 100644 --- a/README.md +++ b/README.md @@ -695,6 +695,7 @@ GitLens is highly customizable and provides many configuration settings to allow | `gitlens.hovers.annotations.enabled` | Specifies whether to provide any hovers when showing blame annotations | | `gitlens.hovers.annotations.over` | Specifies when to trigger hovers when showing blame annotations

`annotation` - only shown when hovering over the line annotation
`line` - shown when hovering anywhere over the line | | `gitlens.hovers.avatars` | Specifies whether to show avatar images in hovers | +| `gitlens.hovers.changesDiff` | Specifies whether to show just the changes to the line or the full set of related changes in the _changes (diff)_ hover

`line` - Shows only the changes to the line

`hunk` - Shows the full set of related changes | | `gitlens.hovers.currentLine.changes` | Specifies whether to provide a _changes (diff)_ hover for the current line | | `gitlens.hovers.currentLine.details` | Specifies whether to provide a _commit details_ hover for the current line | | `gitlens.hovers.currentLine.enabled` | Specifies whether to provide any hovers for the current line | diff --git a/package.json b/package.json index d122719..49bf677 100644 --- a/package.json +++ b/package.json @@ -563,6 +563,20 @@ "markdownDescription": "Specifies whether to show avatar images in hovers", "scope": "window" }, + "gitlens.hovers.changesDiff": { + "type": "string", + "default": "line", + "enum": [ + "line", + "hunk" + ], + "enumDescriptions": [ + "Shows only the changes to the line", + "Shows the full set of related changes" + ], + "markdownDescription": "Specifies whether to show just the changes to the line or the full set of related changes in the _changes (diff)_ hover", + "scope": "window" + }, "gitlens.hovers.detailsMarkdownFormat": { "type": "string", "default": "[${avatar}  __${author}__](mailto:${email}), ${ago}   _(${date})_ \n\n${message}\n\n${commands}", diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index a95bd46..7f2878a 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -14,8 +14,9 @@ import { Container } from '../container'; import { CommitFormatOptions, CommitFormatter, + GitBlameCommit, GitCommit, - GitDiffChunkLine, + GitDiffHunkLine, GitRemote, GitService, GitUri @@ -108,11 +109,11 @@ export class Annotations { static getHoverDiffMessage( commit: GitCommit, uri: GitUri, - chunkLine: GitDiffChunkLine | undefined + hunkLine: GitDiffHunkLine | undefined ): MarkdownString | undefined { - if (chunkLine === undefined || commit.previousSha === undefined) return undefined; + if (hunkLine === undefined || commit.previousSha === undefined) return undefined; - const codeDiff = this.getCodeDiff(chunkLine); + const diff = this.getDiffFromHunkLine(hunkLine); let message: string; if (commit.isUncommitted) { @@ -121,12 +122,12 @@ export class Annotations { GlyphChars.Dash }   [\`${commit.previousShortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs( commit.previousSha! - )} "Show Commit Details") ${GlyphChars.ArrowLeftRightLong} _${uri.shortSha}_\n${codeDiff}`; + )} "Show Commit Details") ${GlyphChars.ArrowLeftRightLong} _${uri.shortSha}_\n${diff}`; } else { message = `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)} "Open Changes")   ${ GlyphChars.Dash - }   _uncommitted changes_\n${codeDiff}`; + }   _uncommitted changes_\n${diff}`; } } else { @@ -136,9 +137,7 @@ export class Annotations { commit.previousSha! )} "Show Commit Details") ${GlyphChars.ArrowLeftRightLong} [\`${ commit.shortSha - }\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs( - commit.sha - )} "Show Commit Details")\n${codeDiff}`; + }\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)} "Show Commit Details")\n${diff}`; } const markdown = new MarkdownString(message); @@ -146,21 +145,36 @@ export class Annotations { return markdown; } - private static getCodeDiff(chunkLine: GitDiffChunkLine): string { - const previous = chunkLine.previous === undefined ? undefined : chunkLine.previous[0]; - return `\`\`\` -- ${previous === undefined || previous.line === undefined ? '' : previous.line.trim()} -+ ${chunkLine.line === undefined ? '' : chunkLine.line.trim()} -\`\`\``; + private static getDiffFromHunkLine(hunkLine: GitDiffHunkLine): string { + if (Container.config.hovers.changesDiff === 'hunk') { + return `\`\`\`diff\n${hunkLine.hunk.diff}\n\`\`\``; + } + + return `\`\`\`diff${hunkLine.previous === undefined ? '' : `\n-${hunkLine.previous.line}`}${ + hunkLine.current === undefined ? '' : `\n+${hunkLine.current.line}` + }\n\`\`\``; } - static async changesHover(commit: GitCommit, line: number, uri: GitUri): Promise> { - const sha = - !commit.isUncommitted || (uri.sha !== undefined && GitService.isStagedUncommitted(uri.sha)) - ? commit.previousSha - : undefined; - const chunkLine = await Container.git.getDiffForLine(uri, line, sha); - const message = this.getHoverDiffMessage(commit, uri, chunkLine); + static async changesHover( + commit: GitBlameCommit, + editorLine: number, + uri: GitUri + ): Promise> { + let ref; + if (commit.isUncommitted) { + if (uri.sha !== undefined && GitService.isStagedUncommitted(uri.sha)) { + ref = uri.sha; + } + } + else { + ref = commit.sha; + } + + const line = editorLine + 1; + const commitLine = commit.lines.find(l => l.line === line) || commit.lines[0]; + + const hunkLine = await Container.git.getDiffForLine(uri, commitLine.originalLine - 1, ref); + const message = this.getHoverDiffMessage(commit, uri, hunkLine); return { hoverMessage: message diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index f31d731..3b2747c 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -11,7 +11,7 @@ import { TextEditorDecorationType } from 'vscode'; import { Container } from '../container'; -import { GitBlame, GitCommit, GitUri } from '../git/gitService'; +import { GitBlame, GitBlameCommit, GitCommit, GitUri } from '../git/gitService'; import { Arrays, Iterables, log } from '../system'; import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker'; import { AnnotationProviderBase } from './annotationProvider'; @@ -88,7 +88,8 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase const highlightDecorationRanges = Arrays.filterMap(blame.lines, l => l.sha === sha - ? this.editor.document.validateRange(new Range(l.line, 0, l.line, Number.MAX_SAFE_INTEGER)) + ? // editor lines are 0-based + this.editor.document.validateRange(new Range(l.line - 1, 0, l.line - 1, Number.MAX_SAFE_INTEGER)) : undefined ); @@ -255,7 +256,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase ); } - private async getCommitForHover(position: Position): Promise { + private async getCommitForHover(position: Position): Promise { if (Container.config.hovers.annotations.over !== 'line' && position.character !== 0) return undefined; const blame = await this.getBlame(); diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index e9a8c60..c820ba2 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -58,7 +58,8 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { } for (const l of blame.lines) { - const line = l.line; + // editor lines are 0-based + const editorLine = l.line - 1; if (previousSha === l.sha) { if (gutter === undefined) continue; @@ -84,7 +85,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { compacted = true; } - gutter.range = new Range(line, 0, line, 0); + gutter.range = new Range(editorLine, 0, editorLine, 0); this.decorations.push(gutter); @@ -105,7 +106,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { if (gutter !== undefined) { gutter = { ...gutter, - range: new Range(line, 0, line, 0) + range: new Range(editorLine, 0, editorLine, 0) }; this.decorations.push(gutter); @@ -123,7 +124,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { Annotations.applyHeatmap(gutter, commit.date, computedHeatmap); } - gutter.range = new Range(line, 0, line, 0); + gutter.range = new Range(editorLine, 0, editorLine, 0); this.decorations.push(gutter); diff --git a/src/annotations/heatmapBlameAnnotationProvider.ts b/src/annotations/heatmapBlameAnnotationProvider.ts index 8aee28d..1bcd477 100644 --- a/src/annotations/heatmapBlameAnnotationProvider.ts +++ b/src/annotations/heatmapBlameAnnotationProvider.ts @@ -31,13 +31,14 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase const computedHeatmap = this.getComputedHeatmap(blame); for (const l of blame.lines) { - const line = l.line; + // editor lines are 0-based + const editorLine = l.line - 1; heatmap = decorationsMap[l.sha]; if (heatmap !== undefined) { heatmap = { ...heatmap, - range: new Range(line, 0, line, 0) + range: new Range(editorLine, 0, editorLine, 0) }; this.decorations.push(heatmap); @@ -49,7 +50,7 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase if (commit === undefined) continue; heatmap = Annotations.heatmap(commit, computedHeatmap, renderOptions) as DecorationOptions; - heatmap.range = new Range(line, 0, line, 0); + heatmap.range = new Range(editorLine, 0, editorLine, 0); this.decorations.push(heatmap); decorationsMap[l.sha] = heatmap; diff --git a/src/annotations/recentChangesAnnotationProvider.ts b/src/annotations/recentChangesAnnotationProvider.ts index 2f183d9..99fbce9 100644 --- a/src/annotations/recentChangesAnnotationProvider.ts +++ b/src/annotations/recentChangesAnnotationProvider.ts @@ -32,7 +32,7 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase { const commit = await Container.git.getRecentLogCommitForFile(this._uri.repoPath, this._uri.fsPath); if (commit === undefined) return false; - const diff = await Container.git.getDiffForFile(this._uri, commit.previousSha); + const diff = await Container.git.getDiffForFile(this._uri, commit.sha); if (diff === undefined) return false; let start = process.hrtime(); @@ -42,14 +42,15 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase { this.decorations = []; - for (const chunk of diff.chunks) { - let count = chunk.currentPosition.start - 2; - for (const line of chunk.lines) { - if (line.line === undefined) continue; + for (const hunk of diff.hunks) { + // Subtract 2 because editor lines are 0-based and we will be adding 1 in the first iteration of the loop + let count = hunk.currentPosition.start - 2; + for (const line of hunk.lines) { + if (line.current === undefined) continue; count++; - if (line.state === 'unchanged') continue; + if (line.current.state === 'unchanged') continue; const range = this.editor.document.validateRange( new Range(new Position(count, 0), new Position(count, Number.MAX_SAFE_INTEGER)) diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index c5eacf3..b4fc21f 100644 --- a/src/commands/diffLineWithWorking.ts +++ b/src/commands/diffLineWithWorking.ts @@ -59,7 +59,8 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand { previousSha: null, previousFileName: null }); - args.line = blame.line.line + 1; + // editor lines are 0-based + args.line = blame.line.line - 1; } } catch (ex) { diff --git a/src/config.ts b/src/config.ts index 366c7c4..6efcd36 100644 --- a/src/config.ts +++ b/src/config.ts @@ -51,6 +51,7 @@ export interface Config { over: 'line' | 'annotation'; }; avatars: boolean; + changesDiff: 'line' | 'hunk'; detailsMarkdownFormat: string; enabled: boolean; }; diff --git a/src/git/git.ts b/src/git/git.ts index 864807b..0bc2747 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -518,7 +518,7 @@ export class Git { ref2?: string, options: { encoding?: string; filter?: string } = {} ): Promise { - const params = ['diff', '-M', '--no-ext-diff', '--minimal']; + const params = ['diff', '-M', '--no-ext-diff', '-U0', '--minimal']; if (options.filter) { params.push(`--diff-filter=${options.filter}`); } @@ -866,6 +866,10 @@ export class Git { } } + static show_diff(repoPath: string, fileName: string, ref: string) { + return git({ cwd: repoPath }, 'show', '--format=', '--minimal', '-U0', ref, '--', fileName); + } + static show_status(repoPath: string, fileName: string, ref: string) { return git({ cwd: repoPath }, 'show', '--name-status', '--format=', ref, '--', fileName); } diff --git a/src/git/gitService.ts b/src/git/gitService.ts index c3d339f..422a418 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -43,7 +43,7 @@ import { GitCommitType, GitContributor, GitDiff, - GitDiffChunkLine, + GitDiffHunkLine, GitDiffParser, GitDiffShortStat, GitFile, @@ -139,7 +139,7 @@ export class GitService implements Disposable { this._disposable && this._disposable.dispose(); } - get UseCaching() { + get useCaching() { return Container.config.advanced.caching.enabled; } @@ -749,7 +749,7 @@ export class GitService implements Disposable { } const doc = await Container.tracker.getOrAdd(uri); - if (this.UseCaching) { + if (this.useCaching) { if (doc.state !== undefined) { const cachedBlame = doc.state.get(key); if (cachedBlame !== undefined) { @@ -832,7 +832,7 @@ export class GitService implements Disposable { const key = `blame:${Strings.sha1(contents)}`; const doc = await Container.tracker.getOrAdd(uri); - if (this.UseCaching) { + if (this.useCaching) { if (doc.state !== undefined) { const cachedBlame = doc.state.get(key); if (cachedBlame !== undefined) { @@ -908,17 +908,17 @@ export class GitService implements Disposable { @log() async getBlameForLine( uri: GitUri, - line: number, + editorLine: number, // editor lines are 0-based options: { skipCache?: boolean } = {} ): Promise { - if (!options.skipCache && this.UseCaching) { + if (!options.skipCache && this.useCaching) { const blame = await this.getBlameForFile(uri); if (blame === undefined) return undefined; - let blameLine = blame.lines[line]; + let blameLine = blame.lines[editorLine]; if (blameLine === undefined) { - if (blame.lines.length !== line) return undefined; - blameLine = blame.lines[line - 1]; + if (blame.lines.length !== editorLine) return undefined; + blameLine = blame.lines[editorLine - 1]; } const commit = blame.commits.get(blameLine.sha); @@ -932,7 +932,7 @@ export class GitService implements Disposable { }; } - const lineToBlame = line + 1; + const lineToBlame = editorLine + 1; const fileName = uri.fsPath; try { @@ -948,7 +948,7 @@ export class GitService implements Disposable { return { author: Iterables.first(blame.authors.values()), commit: Iterables.first(blame.commits.values()), - line: blame.lines[line] + line: blame.lines[editorLine] }; } catch { @@ -963,18 +963,18 @@ export class GitService implements Disposable { }) async getBlameForLineContents( uri: GitUri, - line: number, + editorLine: number, // editor lines are 0-based contents: string, options: { skipCache?: boolean } = {} ): Promise { - if (!options.skipCache && this.UseCaching) { + if (!options.skipCache && this.useCaching) { const blame = await this.getBlameForFileContents(uri, contents); if (blame === undefined) return undefined; - let blameLine = blame.lines[line]; + let blameLine = blame.lines[editorLine]; if (blameLine === undefined) { - if (blame.lines.length !== line) return undefined; - blameLine = blame.lines[line - 1]; + if (blame.lines.length !== editorLine) return undefined; + blameLine = blame.lines[editorLine - 1]; } const commit = blame.commits.get(blameLine.sha); @@ -988,7 +988,7 @@ export class GitService implements Disposable { }; } - const lineToBlame = line + 1; + const lineToBlame = editorLine + 1; const fileName = uri.fsPath; try { @@ -1005,7 +1005,7 @@ export class GitService implements Disposable { return { author: Iterables.first(blame.authors.values()), commit: Iterables.first(blame.commits.values()), - line: blame.lines[line] + line: blame.lines[editorLine] }; } catch { @@ -1034,13 +1034,17 @@ export class GitService implements Disposable { const lines = blame.lines.slice(range.start.line, range.end.line + 1); const shas = new Set(lines.map(l => l.sha)); + // ranges are 0-based + const startLine = range.start.line + 1; + const endLine = range.end.line + 1; + const authors: Map = new Map(); const commits: Map = new Map(); for (const c of blame.commits.values()) { if (!shas.has(c.sha)) continue; const commit = c.with({ - lines: c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line) + lines: c.lines.filter(l => l.line >= startLine && l.line <= endLine) }); commits.set(c.sha, commit); @@ -1165,13 +1169,9 @@ export class GitService implements Disposable { } @log() - async getDiffForFile(uri: GitUri, ref1?: string, ref2?: string): Promise { + async getDiffForFile(uri: GitUri, ref1: string | undefined, ref2?: string): Promise { const cc = Logger.getCorrelationContext(); - if (ref1 !== undefined && ref2 === undefined && uri.sha !== undefined) { - ref2 = uri.sha; - } - let key = 'diff'; if (ref1 !== undefined) { key += `:${ref1}`; @@ -1181,7 +1181,7 @@ export class GitService implements Disposable { } const doc = await Container.tracker.getOrAdd(uri); - if (this.UseCaching) { + if (this.useCaching) { if (doc.state !== undefined) { const cachedDiff = doc.state.get(key); if (cachedDiff !== undefined) { @@ -1233,7 +1233,14 @@ export class GitService implements Disposable { const [file, root] = Git.splitPath(fileName, repoPath, false); try { - const data = await Git.diff(root, file, ref1, ref2, { ...options, filter: 'M' }); + let data; + if (ref1 !== undefined && ref2 === undefined && !GitService.isStagedUncommitted(ref1)) { + data = await Git.show_diff(root, file, ref1); + } + else { + data = await Git.diff(root, file, ref1, ref2, { ...options, filter: 'M' }); + } + const diff = GitDiffParser.parse(data); return diff; } @@ -1259,18 +1266,24 @@ export class GitService implements Disposable { @log() async getDiffForLine( uri: GitUri, - line: number, - ref1?: string, + editorLine: number, // editor lines are 0-based + ref1: string | undefined, ref2?: string - ): Promise { + ): Promise { try { - const diff = await this.getDiffForFile(uri, ref1, ref2); + let diff = await this.getDiffForFile(uri, ref1, ref2); + // If we didn't find a diff & ref1 is undefined (meaning uncommitted), check for a staged diff + if (diff === undefined && ref1 === undefined) { + diff = await this.getDiffForFile(uri, Git.stagedUncommittedSha, ref2); + } + if (diff === undefined) return undefined; - const chunk = diff.chunks.find(c => c.currentPosition.start <= line && c.currentPosition.end >= line); - if (chunk === undefined) return undefined; + const line = editorLine + 1; + const hunk = diff.hunks.find(c => c.currentPosition.start <= line && c.currentPosition.end >= line); + if (hunk === undefined) return undefined; - return chunk.lines[line - chunk.currentPosition.start + 1]; + return hunk.lines[line - hunk.currentPosition.start]; } catch (ex) { return undefined; @@ -1480,7 +1493,7 @@ export class GitService implements Disposable { } const doc = await Container.tracker.getOrAdd(GitUri.fromFile(fileName, repoPath!, options.ref)); - if (this.UseCaching && options.range === undefined) { + if (this.useCaching && options.range === undefined) { if (doc.state !== undefined) { const cachedLog = doc.state.get(key); if (cachedLog !== undefined) { diff --git a/src/git/models/diff.ts b/src/git/models/diff.ts index fa9ea0d..0bbec44 100644 --- a/src/git/models/diff.ts +++ b/src/git/models/diff.ts @@ -6,26 +6,24 @@ export interface GitDiffLine { state: 'added' | 'removed' | 'unchanged'; } -export interface GitDiffChunkLine extends GitDiffLine { - previous?: (GitDiffLine | undefined)[]; +export interface GitDiffHunkLine { + hunk: GitDiffHunk; + current: GitDiffLine | undefined; + previous: GitDiffLine | undefined; } -export class GitDiffChunk { - private _chunk: string | undefined; - private _lines: GitDiffChunkLine[] | undefined; +export class GitDiffHunk { + private _lines: GitDiffHunkLine[] | undefined; constructor( - chunk: string, + public readonly diff: string, public currentPosition: { start: number; end: number }, public previousPosition: { start: number; end: number } - ) { - this._chunk = chunk; - } + ) {} - get lines(): GitDiffChunkLine[] { + get lines(): GitDiffHunkLine[] { if (this._lines === undefined) { - this._lines = GitDiffParser.parseChunk(this._chunk!); - this._chunk = undefined; + this._lines = GitDiffParser.parseHunk(this); } return this._lines; @@ -33,7 +31,7 @@ export class GitDiffChunk { } export interface GitDiff { - readonly chunks: GitDiffChunk[]; + readonly hunks: GitDiffHunk[]; readonly diff?: string; } diff --git a/src/git/parsers/blameParser.ts b/src/git/parsers/blameParser.ts index 617f315..37fcb56 100644 --- a/src/git/parsers/blameParser.ts +++ b/src/git/parsers/blameParser.ts @@ -55,8 +55,8 @@ export class GitBlameParser { entry = { author: undefined!, sha: lineParts[0], - originalLine: parseInt(lineParts[1], 10) - 1, - line: parseInt(lineParts[2], 10) - 1, + originalLine: parseInt(lineParts[1], 10), + line: parseInt(lineParts[2], 10), lineCount: parseInt(lineParts[3], 10) }; @@ -230,7 +230,7 @@ export class GitBlameParser { } commit.lines.push(line); - lines[line.line] = line; + lines[line.line - 1] = line; } } } diff --git a/src/git/parsers/diffParser.ts b/src/git/parsers/diffParser.ts index da6d830..a9b5641 100644 --- a/src/git/parsers/diffParser.ts +++ b/src/git/parsers/diffParser.ts @@ -1,63 +1,70 @@ 'use strict'; -import { Iterables, Strings } from '../../system'; -import { GitDiff, GitDiffChunk, GitDiffChunkLine, GitDiffLine, GitDiffShortStat, GitFile, GitFileStatus } from '../git'; +import { Strings } from '../../system'; +import { GitDiff, GitDiffHunk, GitDiffHunkLine, GitDiffLine, GitDiffShortStat, GitFile, GitFileStatus } from '../git'; const nameStatusDiffRegex = /^(.*?)\t(.*?)(?:\t(.*?))?$/gm; const shortStatDiffRegex = /^\s*(\d+)\sfiles? changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/; -const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm; +const unifiedDiffRegex = /^@@ -([\d]+)(?:,([\d]+))? \+([\d]+)(?:,([\d]+))? @@(?:.*?)\n([\s\S]*?)(?=^@@)/gm; export class GitDiffParser { static parse(data: string, debug: boolean = false): GitDiff | undefined { if (!data) return undefined; - const chunks: GitDiffChunk[] = []; + const hunks: GitDiffHunk[] = []; let match: RegExpExecArray | null; - let chunk; + let hunk; + let currentStartStr; let currentStart; + let currentCountStr; + let currentCount; + let previousStartStr; let previousStart; - + let previousCountStr; + let previousCount; do { match = unifiedDiffRegex.exec(`${data}\n@@`); if (match == null) break; // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - chunk = ` ${match[5]}`.substr(1); - currentStart = parseInt(match[3], 10); - previousStart = parseInt(match[1], 10); + hunk = ` ${match[5]}`.substr(1); + + [, previousStartStr, previousCountStr, currentStartStr, currentCountStr] = match; + previousStart = parseInt(previousStartStr, 10); + previousCount = previousCountStr ? parseInt(previousCountStr, 10) : 0; + currentStart = parseInt(currentStartStr, 10); + currentCount = currentCountStr ? parseInt(currentCountStr, 10) : 0; - chunks.push( - new GitDiffChunk( - chunk, + hunks.push( + new GitDiffHunk( + hunk, { start: currentStart, - end: currentStart + parseInt(match[4], 10) + end: currentStart + currentCount }, { start: previousStart, - end: previousStart + parseInt(match[2], 10) + end: previousStart + previousCount } ) ); } while (match != null); - if (!chunks.length) return undefined; + if (!hunks.length) return undefined; const diff: GitDiff = { diff: debug ? data : undefined, - chunks: chunks + hunks: hunks }; return diff; } - static parseChunk(chunk: string): GitDiffChunkLine[] { - const lines = Iterables.skip(Strings.lines(chunk), 1); - + static parseHunk(hunk: GitDiffHunk): GitDiffHunkLine[] { const currentLines: (GitDiffLine | undefined)[] = []; const previousLines: (GitDiffLine | undefined)[] = []; let removed = 0; - for (const l of lines) { + for (const l of Strings.lines(hunk.diff)) { switch (l[0]) { case '+': currentLines.push({ @@ -97,35 +104,22 @@ export class GitDiffParser { } } - const chunkLines: GitDiffChunkLine[] = []; + while (removed > 0) { + removed--; + currentLines.push(undefined); + } - let chunkLine: GitDiffChunkLine | undefined = undefined; - let current: GitDiffLine | undefined = undefined; + const hunkLines: GitDiffHunkLine[] = []; for (let i = 0; i < currentLines.length; i++) { - current = currentLines[i]; - if (current === undefined) { - // Don't think we need to worry about this case because the diff will always have "padding" (i.e. unchanged lines) around each chunk - if (chunkLine === undefined) continue; - - if (chunkLine.previous === undefined) { - chunkLine.previous = [previousLines[i]]; - continue; - } - - chunkLine.previous.push(previousLines[i]); - continue; - } - - chunkLine = { - line: current.line, - state: current.state, - previous: [previousLines[i]] - }; - chunkLines.push(chunkLine); + hunkLines.push({ + hunk: hunk, + current: currentLines[i], + previous: previousLines[i] + }); } - return chunkLines; + return hunkLines; } static parseNameStatus(data: string, repoPath: string): GitFile[] | undefined { diff --git a/src/trackers/gitLineTracker.ts b/src/trackers/gitLineTracker.ts index d2f4899..16c4821 100644 --- a/src/trackers/gitLineTracker.ts +++ b/src/trackers/gitLineTracker.ts @@ -123,7 +123,7 @@ export class GitLineTracker extends LineTracker { : await Container.git.getBlameForLine(trackedDocument.uri, lines[0]); if (blameLine === undefined) return false; - this.setState(blameLine.line.line, new GitLineState(blameLine.commit)); + this.setState(blameLine.line.line - 1, new GitLineState(blameLine.commit)); } else { const blame = editor.document.isDirty